@arronqzy/react-blueprint 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @arronqzy/react-blueprint
2
+
3
+ Abuilder 的**蓝图编辑器** React 包,基于 [React Flow](https://reactflow.dev/) 提供节点连线编辑、调试执行与蓝图库管理。
4
+
5
+ ## 功能概览
6
+
7
+ - **可视化蓝图**:拖拽节点、连线、对齐、视口平移缩放
8
+ - **节点类型**:逻辑、请求、时钟、JSON、子蓝图引用等(由 `blueprint-dsl` 定义行为)
9
+ - **调试执行**:单步/连续调试、执行轨迹、连线信号高亮、时钟节点进度
10
+ - **蓝图库**:IndexedDB 存储、导入导出、元数据编辑
11
+ - **执行日志**:运行记录持久化、导出与清理策略
12
+ - **视图联动**:生命周期钩子向视图节点写入 Scope
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ pnpm add @arronqzy/react-blueprint @xyflow/react
18
+ ```
19
+
20
+ ```ts
21
+ import "@arronqzy/react-blueprint/blueprint.css";
22
+ ```
23
+
24
+ ## 使用
25
+
26
+ ### 蓝图画布
27
+
28
+ ```tsx
29
+ import { useState } from "react";
30
+ import { BluePrintReactRoot, BlueprintGraph } from "@arronqzy/react-blueprint";
31
+ import "@arronqzy/react-blueprint/blueprint.css";
32
+
33
+ function BlueprintEditor() {
34
+ const [graph, setGraph] = useState(() => BlueprintGraph.empty());
35
+
36
+ return (
37
+ <BluePrintReactRoot
38
+ graph={graph}
39
+ onGraphChange={setGraph}
40
+ selectedNodeId={null}
41
+ onSelectNode={() => {}}
42
+ />
43
+ );
44
+ }
45
+ ```
46
+
47
+ ### 文档转可执行图
48
+
49
+ ```ts
50
+ import { documentToRunnableGraph } from "@arronqzy/react-blueprint";
51
+
52
+ const runnable = documentToRunnableGraph(document, { libraryNameById });
53
+ ```
54
+
55
+ ### 调试会话
56
+
57
+ ```tsx
58
+ import { useBlueprintDebugSession } from "@arronqzy/react-blueprint";
59
+
60
+ const debug = useBlueprintDebugSession({
61
+ graph,
62
+ blueprintId,
63
+ blueprintName: "我的蓝图",
64
+ resolveLibraryBlueprint,
65
+ onViewScopeUpdate: (ids, scope) => { /* 回写视图 Scope */ },
66
+ });
67
+ ```
68
+
69
+ ### 页面生命周期(与视图集成)
70
+
71
+ ```tsx
72
+ import { useBlueprintPageLifecycle } from "@arronqzy/react-blueprint";
73
+
74
+ useBlueprintPageLifecycle({
75
+ graph,
76
+ active: blueprintOpen,
77
+ onViewScopeUpdate,
78
+ resolveLibraryBlueprint,
79
+ });
80
+ ```
81
+
82
+ ## 主要导出
83
+
84
+ | 模块 | 说明 |
85
+ |------|------|
86
+ | `BluePrintReactRoot` | 蓝图主画布 |
87
+ | `BlueprintGraph` | 图数据结构(`src/graph`) |
88
+ | `documentToRunnableGraph` | 文档 → 运行时图 |
89
+ | `useBlueprintDebugSession` | 调试工具栏与会话 |
90
+ | `useBlueprintPageLifecycle` | 挂载/卸载时启停蓝图 |
91
+ | `BlueprintNodeConfigSidebar` | 节点配置侧栏 |
92
+ | `BlueprintPanelToolbar` | 蓝图工具栏 |
93
+ | `blueprint-library-db` | 蓝图库 IndexedDB API |
94
+
95
+ ## 依赖关系
96
+
97
+ - `@arronqzy/blueprint-dsl` — 节点定义与执行引擎
98
+ - `@arronqzy/react-rx-store` — 与画布状态订阅
99
+ - `@arronqzy/ui` — UI 组件
100
+
101
+ ## 许可证
102
+
103
+ ISC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/react-blueprint",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "",
5
5
  "main": "index.ts",
6
6
  "exports": {
@@ -12,9 +12,9 @@
12
12
  "dayjs": "^1.11.13",
13
13
  "react-dom": "^19",
14
14
  "@xyflow/react": "^12.9.3",
15
- "@arronqzy/blueprint-dsl": "1.0.1",
16
- "@arronqzy/react-rx-store": "1.1.1",
17
- "@arronqzy/ui": "1.0.0"
15
+ "@arronqzy/blueprint-dsl": "1.0.3",
16
+ "@arronqzy/react-rx-store": "1.1.2",
17
+ "@arronqzy/ui": "1.0.2"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/react": "^19.0.1",
@@ -27,9 +27,9 @@
27
27
  "eslint": "^8.57.0",
28
28
  "typescript": "^5.5.4",
29
29
  "vite": "^5.1.4",
30
+ "@arronqzy/tailwind": "1.0.2",
30
31
  "@arronqzy/eslint-config": "0.0.0",
31
- "@arronqzy/typescript-config": "1.0.0",
32
- "@arronqzy/tailwind": "1.0.1"
32
+ "@arronqzy/typescript-config": "1.0.0"
33
33
  },
34
34
  "keywords": [],
35
35
  "author": "",
@@ -87,6 +87,8 @@ export function BlueprintNodeConfigSidebar({
87
87
 
88
88
  useEffect(() => {
89
89
  if (configSource !== "view") return;
90
+ // 视图画布尚未就绪时不清理,避免误删有效绑定导致绑定节点「执行了但不写 scope」
91
+ if (viewElementOptions.length === 0) return;
90
92
  const linked = resolveViewElementIds(node);
91
93
  if (linked.length === 0) return;
92
94
  const pruned = pruneViewElementIds(linked, existingViewElementIdSet);
@@ -103,6 +105,7 @@ export function BlueprintNodeConfigSidebar({
103
105
  node.viewElementId,
104
106
  node.viewElementIds,
105
107
  onUpdateNode,
108
+ viewElementOptions.length,
106
109
  ]);
107
110
 
108
111
  return (
package/src/blueprint.css CHANGED
@@ -19,6 +19,8 @@
19
19
  --xy-edge-stroke-selected: #1d4ed8;
20
20
  --xy-connectionline-stroke: #2563eb;
21
21
  --xy-connectionline-stroke-width: 2.5;
22
+ --xy-handle-background-color: hsl(var(--primary));
23
+ --xy-handle-border-color: hsl(var(--background));
22
24
  background-color: transparent;
23
25
  }
24
26
 
@@ -174,11 +176,11 @@
174
176
  height: 10px;
175
177
  min-width: 10px;
176
178
  min-height: 10px;
177
- border: 2px solid hsl(var(--background));
179
+ border: 2px solid hsl(var(--background)) !important;
178
180
  border-radius: 9999px;
179
- background: hsl(var(--muted-foreground) / 0.45);
181
+ background-color: hsl(var(--foreground) / 0.42) !important;
180
182
  transition:
181
- background 0.15s ease,
183
+ background-color 0.15s ease,
182
184
  border-color 0.15s ease,
183
185
  box-shadow 0.15s ease;
184
186
  }
@@ -186,30 +188,34 @@
186
188
  .bp-canvas .react-flow__handle.bp-flow-handle:hover,
187
189
  .bp-flow .react-flow__handle.bp-flow-handle:hover,
188
190
  [data-workspace-region="blueprint"] .react-flow__handle.bp-flow-handle:hover {
189
- background: hsl(var(--primary));
190
- border-color: hsl(var(--background));
191
+ background-color: hsl(var(--primary)) !important;
192
+ border-color: hsl(var(--background)) !important;
191
193
  box-shadow: 0 0 0 3px hsl(var(--primary) / 0.28);
192
194
  }
193
195
 
194
196
  .bp-canvas .react-flow__handle.bp-flow-handle--target,
195
197
  .bp-flow .react-flow__handle.bp-flow-handle--target,
196
198
  [data-workspace-region="blueprint"] .react-flow__handle.bp-flow-handle--target {
197
- background: hsl(var(--muted-foreground) / 0.35);
199
+ background-color: hsl(var(--foreground) / 0.48) !important;
198
200
  }
199
201
 
200
202
  .bp-canvas .react-flow__handle.bp-flow-handle--source,
201
203
  .bp-flow .react-flow__handle.bp-flow-handle--source,
202
204
  [data-workspace-region="blueprint"] .react-flow__handle.bp-flow-handle--source {
203
- background: hsl(var(--primary) / 0.55);
205
+ background-color: hsl(var(--primary) / 0.72) !important;
204
206
  }
205
207
 
206
208
  .bp-node--selected .bp-flow-handle {
207
- border-color: hsl(var(--card));
209
+ border-color: hsl(var(--card)) !important;
208
210
  box-shadow: 0 0 0 2px hsl(var(--primary) / 0.35);
209
211
  }
210
212
 
213
+ .bp-node--selected .bp-flow-handle--target {
214
+ background-color: hsl(var(--foreground) / 0.55) !important;
215
+ }
216
+
211
217
  .bp-node--selected .bp-flow-handle--source {
212
- background: hsl(var(--primary));
218
+ background-color: hsl(var(--primary)) !important;
213
219
  }
214
220
 
215
221
  .bp-node--execution-true .bp-node-card {
@@ -225,11 +231,11 @@
225
231
  }
226
232
 
227
233
  .bp-node--execution-true .bp-flow-handle--source {
228
- background: #16a34a;
234
+ background-color: #16a34a !important;
229
235
  }
230
236
 
231
237
  .bp-node--execution-true .bp-flow-handle--target {
232
- background: rgb(22 163 74 / 0.55);
238
+ background-color: rgb(22 163 74 / 0.55) !important;
233
239
  }
234
240
 
235
241
  .bp-node--execution-false .bp-node-card {
@@ -245,11 +251,11 @@
245
251
  }
246
252
 
247
253
  .bp-node--execution-false .bp-flow-handle--source {
248
- background: hsl(var(--destructive));
254
+ background-color: hsl(var(--destructive)) !important;
249
255
  }
250
256
 
251
257
  .bp-node--execution-false .bp-flow-handle--target {
252
- background: hsl(var(--destructive) / 0.55);
258
+ background-color: hsl(var(--destructive) / 0.55) !important;
253
259
  }
254
260
 
255
261
  /* 生命周期节点无输入口:隐藏任何左侧/目标连接点(含旧数据产生的幽灵 handle) */
@@ -267,13 +273,13 @@
267
273
  .bp-canvas .react-flow__handle.bp-flow-handle--signal,
268
274
  .bp-flow .react-flow__handle.bp-flow-handle--signal,
269
275
  [data-workspace-region="blueprint"] .react-flow__handle.bp-flow-handle--signal {
270
- background: hsl(38 92% 50% / 0.75);
276
+ background-color: hsl(38 92% 50% / 0.75) !important;
271
277
  width: 8px;
272
278
  height: 8px;
273
279
  }
274
280
 
275
281
  .bp-node--selected .bp-flow-handle--signal {
276
- background: hsl(38 92% 50%);
282
+ background-color: hsl(38 92% 50%) !important;
277
283
  }
278
284
 
279
285
  /* 右键菜单 */
@@ -53,8 +53,8 @@ function IconClearDb({ className }: { className?: string }) {
53
53
  aria-hidden="true"
54
54
  >
55
55
  <ellipse cx="12" cy="6" rx="7" ry="2.5" />
56
- <path d="M5 6v12c0 1.5 3.1 2.5 7 2.5s7-1 2.5-7-2.5V6" />
57
- <path d="M5 12c0 1.5 3.1 2.5 7 2.5s7-1 7-2.5" />
56
+ <path d="M5 6v12c0 1.5 3.134 2.5 7 2.5s7-1.343 7-2.5V6" />
57
+ <path d="M5 12c0 1.5 3.134 2.5 7 2.5s7-1.343 7-2.5" />
58
58
  <path d="m16 16 4 4M20 16l-4 4" />
59
59
  </svg>
60
60
  );
@@ -19,6 +19,7 @@ import {
19
19
  FETCH_CREDENTIALS,
20
20
  FETCH_HTTP_METHODS,
21
21
  FETCH_MODES,
22
+ FETCH_NODE_TYPE,
22
23
  FETCH_REDIRECTS,
23
24
  FETCH_RESPONSE_TYPES,
24
25
  } from "@arronqzy/blueprint-dsl";
@@ -238,8 +239,10 @@ function patchFetchConfig(
238
239
  patch: Partial<FetchRequestConfig>
239
240
  ) {
240
241
  return {
241
- fetchConfig: { ...resolveNodeFetchConfig(node), ...patch },
242
+ role: "fetch" as const,
243
+ nodeType: FETCH_NODE_TYPE,
242
244
  configSource: "fetch" as const,
245
+ fetchConfig: { ...resolveNodeFetchConfig(node), ...patch },
243
246
  };
244
247
  }
245
248
 
@@ -8,6 +8,7 @@ import {
8
8
  resolveNodeFetchConfig,
9
9
  resolveNodeJsonConfig,
10
10
  resolveNodeLogicConfig,
11
+ resolveRunnableNodeType,
11
12
  sanitizeBlueprintDocument,
12
13
  type BlueprintDocument,
13
14
  type BlueprintGraphEdge,
@@ -250,7 +251,10 @@ export class BlueprintGraph {
250
251
  ...patch.clockConfig,
251
252
  };
252
253
  }
253
- return nextNode;
254
+ return {
255
+ ...nextNode,
256
+ nodeType: resolveRunnableNodeType(nextNode),
257
+ };
254
258
  }),
255
259
  });
256
260
  }
@@ -89,19 +89,50 @@ export function pruneViewElementIds(
89
89
  export function resolveBlueprintConfigSource(
90
90
  node: Pick<
91
91
  BlueprintGraphNode,
92
- "role" | "configSource" | "viewElementId" | "viewElementIds"
92
+ "role" | "nodeType" | "configSource" | "viewElementId" | "viewElementIds"
93
93
  >
94
94
  ): BlueprintConfigSource {
95
95
  if (node.configSource) return node.configSource;
96
- if (resolveViewElementIds(node).length > 0) return "view";
97
96
  if (node.role === "lifecycle") return "lifecycle";
98
97
  if (node.role === "clock") return "clock";
99
98
  if (node.role === "and") return "and";
100
99
  if (node.role === "fetch") return "fetch";
101
100
  if (node.role === "json") return "json";
101
+ if (node.nodeType === FETCH_NODE_TYPE) return "fetch";
102
+ if (node.nodeType === JSON_NODE_TYPE) return "json";
103
+ if (node.nodeType === CLOCK_NODE_TYPE) return "clock";
104
+ if (node.nodeType === VIEW_NODE_TYPE) return "view";
105
+ if (resolveViewElementIds(node).length > 0) return "view";
102
106
  return node.role === "logic" ? "logic" : "blueprint";
103
107
  }
104
108
 
109
+ /** 可执行图节点类型:与配置类型对齐,避免文档里残留旧 nodeType */
110
+ export function resolveRunnableNodeType(
111
+ node: Pick<
112
+ BlueprintGraphNode,
113
+ "role" | "nodeType" | "configSource" | "viewElementId" | "viewElementIds"
114
+ >
115
+ ): string {
116
+ switch (resolveBlueprintConfigSource(node)) {
117
+ case "view":
118
+ return VIEW_NODE_TYPE;
119
+ case "lifecycle":
120
+ return LIFECYCLE_NODE_TYPE;
121
+ case "clock":
122
+ return CLOCK_NODE_TYPE;
123
+ case "and":
124
+ return AND_NODE_TYPE;
125
+ case "fetch":
126
+ return FETCH_NODE_TYPE;
127
+ case "json":
128
+ return JSON_NODE_TYPE;
129
+ case "logic":
130
+ return DEFAULT_LOGIC_NODE_TYPE;
131
+ default:
132
+ return BLUEPRINT_NODE_TYPE;
133
+ }
134
+ }
135
+
105
136
  /** 画布节点顶部(拖拽区)展示的配置类型文案 */
106
137
  export const BLUEPRINT_CONFIG_TYPE_LABELS: Record<
107
138
  BlueprintConfigSource,
@@ -120,7 +151,7 @@ export const BLUEPRINT_CONFIG_TYPE_LABELS: Record<
120
151
  export function resolveBlueprintNodeTypeLabel(
121
152
  node: Pick<
122
153
  BlueprintGraphNode,
123
- "role" | "configSource" | "viewElementId" | "viewElementIds"
154
+ "role" | "nodeType" | "configSource" | "viewElementId" | "viewElementIds"
124
155
  >
125
156
  ): string {
126
157
  return BLUEPRINT_CONFIG_TYPE_LABELS[resolveBlueprintConfigSource(node)];
@@ -221,11 +252,9 @@ export function sanitizeBlueprintDocument(
221
252
  viewElementId: undefined,
222
253
  };
223
254
  }
224
- if (
225
- resolveBlueprintConfigSource(next) === "view" &&
226
- next.nodeType !== VIEW_NODE_TYPE
227
- ) {
228
- next = { ...next, nodeType: VIEW_NODE_TYPE };
255
+ const runnableNodeType = resolveRunnableNodeType(next);
256
+ if (next.nodeType !== runnableNodeType) {
257
+ next = { ...next, nodeType: runnableNodeType };
229
258
  }
230
259
  if (next.role === "lifecycle") {
231
260
  return { ...next, configSource: "lifecycle" as const };
@@ -233,6 +262,15 @@ export function sanitizeBlueprintDocument(
233
262
  if (next.role === "clock") {
234
263
  return { ...next, configSource: "clock" as const };
235
264
  }
265
+ if (next.role === "fetch") {
266
+ return { ...next, configSource: "fetch" as const };
267
+ }
268
+ if (next.role === "json") {
269
+ return { ...next, configSource: "json" as const };
270
+ }
271
+ if (next.role === "and") {
272
+ return { ...next, configSource: "and" as const };
273
+ }
236
274
  if (next.configSource === "lifecycle" || next.configSource === "clock") {
237
275
  return { ...next, configSource: undefined };
238
276
  }
@@ -42,6 +42,8 @@ export function nodeStructureSignature(
42
42
  lifecyclePhase?: string;
43
43
  libraryBlueprintId?: string;
44
44
  nodeType?: string;
45
+ viewElementId?: string;
46
+ viewElementIds?: string[];
45
47
  fetchConfig?: { url?: string; method?: string; apiBaseUrl?: string; swaggerDocsUrl?: string };
46
48
  jsonConfig?: { jsonString?: string };
47
49
  logicConfig?: { sourceCode?: string };
@@ -51,7 +53,7 @@ export function nodeStructureSignature(
51
53
  return nodes
52
54
  .map(
53
55
  (n) =>
54
- `${n.id}:${n.role}:${n.label}:${n.configSource ?? ""}:${n.lifecyclePhase ?? ""}:${n.libraryBlueprintId ?? ""}:${n.nodeType ?? ""}:${n.fetchConfig?.url ?? ""}:${n.fetchConfig?.method ?? ""}:${n.fetchConfig?.apiBaseUrl ?? ""}:${n.fetchConfig?.swaggerDocsUrl ?? ""}:${n.jsonConfig?.jsonString ?? ""}:${n.logicConfig?.sourceCode ?? ""}:${n.clockConfig?.intervalSeconds ?? ""}:${n.clockConfig?.timeFormat ?? ""}:${n.clockConfig?.outputCount ?? ""}:${n.clockConfig?.emitImmediately ?? ""}`
56
+ `${n.id}:${n.role}:${n.label}:${n.configSource ?? ""}:${n.lifecyclePhase ?? ""}:${n.libraryBlueprintId ?? ""}:${n.nodeType ?? ""}:${(n.viewElementIds ?? (n.viewElementId ? [n.viewElementId] : [])).join(",")}:${n.fetchConfig?.url ?? ""}:${n.fetchConfig?.method ?? ""}:${n.fetchConfig?.apiBaseUrl ?? ""}:${n.fetchConfig?.swaggerDocsUrl ?? ""}:${n.jsonConfig?.jsonString ?? ""}:${n.logicConfig?.sourceCode ?? ""}:${n.clockConfig?.intervalSeconds ?? ""}:${n.clockConfig?.timeFormat ?? ""}:${n.clockConfig?.outputCount ?? ""}:${n.clockConfig?.emitImmediately ?? ""}`
55
57
  )
56
58
  .sort()
57
59
  .join("|");
@@ -112,16 +112,22 @@ export function useBlueprintDebugSession({
112
112
  [graph.document, libraryNameById]
113
113
  );
114
114
 
115
- const syncRunnerWithLatestGraph = useCallback(() => {
115
+ const syncRunnerGraphConfig = useCallback(() => {
116
116
  const runner = runnerRef.current;
117
117
  if (!runner?.isDebugSessionActive()) return;
118
118
  runner.updateGraph(buildRunnableGraph());
119
119
  if (selectedLifecycleNodeId) {
120
120
  runner.refreshDebugLifecycleScope(selectedLifecycleNodeId);
121
121
  }
122
- runner.refreshPendingQueueFromLatestGraph();
123
122
  }, [buildRunnableGraph, selectedLifecycleNodeId]);
124
123
 
124
+ const syncRunnerWithLatestGraph = useCallback(() => {
125
+ syncRunnerGraphConfig();
126
+ const runner = runnerRef.current;
127
+ if (!runner?.isDebugSessionActive()) return;
128
+ runner.refreshPendingQueueFromLatestGraph();
129
+ }, [syncRunnerGraphConfig]);
130
+
125
131
  const resolveBlueprintName = useCallback(
126
132
  (id: string) => libraryNameById?.get(id) ?? id,
127
133
  [libraryNameById]
@@ -242,12 +248,9 @@ export function useBlueprintDebugSession({
242
248
  useEffect(() => {
243
249
  const runner = runnerRef.current;
244
250
  if (!runner?.isDebugSessionActive()) return;
245
- runner.updateGraph(buildRunnableGraph());
246
- if (selectedLifecycleNodeId) {
247
- runner.refreshDebugLifecycleScope(selectedLifecycleNodeId);
248
- }
251
+ syncRunnerGraphConfig();
249
252
  runner.refreshPendingQueueFromLatestGraph();
250
- }, [buildRunnableGraph, graphDebugSig, selectedLifecycleNodeId]);
253
+ }, [graphDebugSig, syncRunnerGraphConfig]);
251
254
 
252
255
  useEffect(() => {
253
256
  if (running) return;
@@ -277,24 +280,14 @@ export function useBlueprintDebugSession({
277
280
  try {
278
281
  const runner = beginRun();
279
282
  if (!runner) return;
283
+ runner.updateGraph(buildRunnableGraph());
284
+ runner.refreshDebugLifecycleScope(selectedLifecycleNodeId);
280
285
  const labels = buildNodeLabelMap(graph);
281
286
  const onProgress = (trace: ExecutionTraceEntry[]) => {
282
287
  setEntries([...trace]);
283
288
  };
284
- while (runner.isDebugSessionActive() && !runner.isDebugQueueEmpty()) {
285
- syncRunnerWithLatestGraph();
286
- await runner.debugStep(labels, onProgress);
287
- const trace = runner.getTraceEntries();
288
- setEntries(trace);
289
- if (shouldHaltDebugOnFalseSignal(trace, allowFalseSignalPropagation)) {
290
- setChainComplete(true);
291
- break;
292
- }
293
- await new Promise<void>((resolve) => {
294
- requestAnimationFrame(() => resolve());
295
- });
296
- }
297
- const result = runner.getTraceEntries();
289
+ const result = await runner.debugRunAll(labels, onProgress);
290
+ setEntries(result);
298
291
  setChainComplete(
299
292
  shouldHaltDebugOnFalseSignal(result, allowFalseSignalPropagation) ||
300
293
  (runner.isDebugQueueEmpty() && result.length > 0)
@@ -304,7 +297,16 @@ export function useBlueprintDebugSession({
304
297
  } finally {
305
298
  setRunning(false);
306
299
  }
307
- }, [allowFalseSignalPropagation, beginRun, buildRunRecord, graph, persistRunIfNeeded, selectedLifecycleNodeId, syncRunnerWithLatestGraph, validateBlueprintGraph]);
300
+ }, [
301
+ allowFalseSignalPropagation,
302
+ beginRun,
303
+ buildRunRecord,
304
+ buildRunnableGraph,
305
+ graph,
306
+ persistRunIfNeeded,
307
+ selectedLifecycleNodeId,
308
+ validateBlueprintGraph,
309
+ ]);
308
310
 
309
311
  const stepNext = useCallback(async () => {
310
312
  if (
@@ -1,8 +1,32 @@
1
- import { useTheme } from "@arronqzy/ui";
1
+ import { useSyncExternalStore } from "react";
2
2
 
3
- /** 与 next-themes 全局主题对齐,勿用 React Flow 的 system(会跟 OS 走) */
4
- export function useBlueprintFlowColorMode(): "light" | "dark" {
5
- const { resolvedTheme, theme } = useTheme();
6
- const current = resolvedTheme ?? theme;
7
- return current === "dark" ? "dark" : "light";
3
+ export type BlueprintColorMode = "light" | "dark";
4
+
5
+ function readDocumentColorMode(): BlueprintColorMode {
6
+ if (typeof document === "undefined") return "light";
7
+ const root = document.documentElement;
8
+ if (root.classList.contains("dark")) return "dark";
9
+ if (root.classList.contains("light")) return "light";
10
+ const dataTheme = root.dataset.theme;
11
+ if (dataTheme === "dark" || dataTheme === "light") return dataTheme;
12
+ return "light";
13
+ }
14
+
15
+ function subscribeDocumentColorMode(onChange: () => void) {
16
+ const root = document.documentElement;
17
+ const observer = new MutationObserver(onChange);
18
+ observer.observe(root, {
19
+ attributes: true,
20
+ attributeFilter: ["class", "data-theme"],
21
+ });
22
+ return () => observer.disconnect();
23
+ }
24
+
25
+ /** 与 document 上的全局主题(class / data-theme)对齐,避免与 panel 手动切换脱节 */
26
+ export function useBlueprintFlowColorMode(): BlueprintColorMode {
27
+ return useSyncExternalStore(
28
+ subscribeDocumentColorMode,
29
+ readDocumentColorMode,
30
+ () => "light"
31
+ );
8
32
  }
@@ -1,10 +1,11 @@
1
1
  import {
2
- VIEW_NODE_TYPE,
2
+ FETCH_NODE_TYPE,
3
3
  type RunnableGraph,
4
4
  } from "@arronqzy/blueprint-dsl";
5
5
 
6
6
  import {
7
- resolveBlueprintConfigSource,
7
+ resolveNodeFetchConfig,
8
+ resolveRunnableNodeType,
8
9
  resolveViewElementIds,
9
10
  type BlueprintDocument,
10
11
  } from "../graph/document";
@@ -17,17 +18,19 @@ export function documentToRunnableGraph(
17
18
  ): RunnableGraph {
18
19
  return {
19
20
  nodes: document.nodes.map((node) => {
20
- const configSource = resolveBlueprintConfigSource(node);
21
21
  const viewElementIds = resolveViewElementIds(node);
22
+ const nodeType = resolveRunnableNodeType(node);
22
23
  return {
23
24
  id: node.id,
24
- nodeType:
25
- configSource === "view" ? VIEW_NODE_TYPE : node.nodeType,
25
+ nodeType,
26
26
  lifecyclePhase: node.lifecyclePhase,
27
27
  libraryBlueprintId: node.libraryBlueprintId,
28
28
  viewElementIds:
29
29
  viewElementIds.length > 0 ? viewElementIds : undefined,
30
- fetchConfig: node.fetchConfig,
30
+ fetchConfig:
31
+ nodeType === FETCH_NODE_TYPE
32
+ ? resolveNodeFetchConfig(node)
33
+ : node.fetchConfig,
31
34
  jsonConfig: node.jsonConfig,
32
35
  logicConfig: node.logicConfig,
33
36
  clockConfig: node.clockConfig,