@arronqzy/react-blueprint 1.0.0

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.
Files changed (124) hide show
  1. package/.eslintrc.cjs +5 -0
  2. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
  3. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
  4. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
  5. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
  6. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
  7. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
  8. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
  9. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
  10. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
  11. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
  12. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
  13. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
  14. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
  15. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
  16. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
  17. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
  18. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
  19. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
  20. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
  21. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
  22. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
  23. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
  24. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
  25. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
  26. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
  27. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
  28. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
  29. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
  30. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
  31. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
  32. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
  33. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
  34. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
  35. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
  36. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
  37. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
  38. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
  39. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
  40. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
  41. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
  42. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
  43. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
  44. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
  45. package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
  46. package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
  47. package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
  48. package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
  49. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
  50. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
  51. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
  52. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
  53. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
  54. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
  55. package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
  56. package/.turbo/turbo-lint.log +4 -0
  57. package/dist/assets/index-CneNgzWb.css +1 -0
  58. package/dist/assets/index-DsVhHV0-.js +226 -0
  59. package/dist/index.html +14 -0
  60. package/index.html +13 -0
  61. package/index.ts +70 -0
  62. package/package.json +43 -0
  63. package/postcss.config.cjs +6 -0
  64. package/src/BlueprintCanvasContext.tsx +61 -0
  65. package/src/BlueprintContextMenu.tsx +108 -0
  66. package/src/BlueprintNodeConfigSidebar.tsx +358 -0
  67. package/src/blueprint.css +321 -0
  68. package/src/blueprintNodeTypes.ts +20 -0
  69. package/src/components/BlueprintExecutionLogPanel.tsx +427 -0
  70. package/src/components/BlueprintLibrarySelect.tsx +68 -0
  71. package/src/components/BlueprintMetaDialog.tsx +96 -0
  72. package/src/components/BlueprintNodeSwitchTaskDialog.tsx +58 -0
  73. package/src/components/BlueprintPanelToolbar.tsx +664 -0
  74. package/src/components/ClockNodeConfigPanel.tsx +139 -0
  75. package/src/components/FetchNodeConfigPanel.tsx +716 -0
  76. package/src/components/FetchUrlAutocomplete.tsx +190 -0
  77. package/src/components/JsonNodeConfigPanel.tsx +79 -0
  78. package/src/components/LogicNodeConfigPanel.tsx +80 -0
  79. package/src/components/ViewElementMultiSelect.tsx +140 -0
  80. package/src/createBlueprintEdgeTypes.tsx +10 -0
  81. package/src/dev-seed.ts +34 -0
  82. package/src/dev.css +38 -0
  83. package/src/edges/BlueprintSmoothEdge.tsx +35 -0
  84. package/src/fetch-config-task-store.ts +197 -0
  85. package/src/flowCoordinates.ts +19 -0
  86. package/src/flowDefaults.ts +9 -0
  87. package/src/graph/blueprint-graph.ts +261 -0
  88. package/src/graph/document.ts +384 -0
  89. package/src/graph/index.ts +7 -0
  90. package/src/graph/node-summary.ts +88 -0
  91. package/src/graph/node-types.ts +9 -0
  92. package/src/graph/react-flow-adapter.ts +127 -0
  93. package/src/graph/sync-edges.ts +69 -0
  94. package/src/graph/sync-nodes.ts +104 -0
  95. package/src/hooks/useBlueprintDebugSession.ts +515 -0
  96. package/src/hooks/useBlueprintFlowColorMode.ts +8 -0
  97. package/src/hooks/useBlueprintFlowState.ts +486 -0
  98. package/src/hooks/useBlueprintFlowViewport.ts +54 -0
  99. package/src/hooks/useBlueprintNodeSelectionGuard.ts +63 -0
  100. package/src/hooks/useBlueprintPageLifecycle.ts +245 -0
  101. package/src/index.tsx +271 -0
  102. package/src/library/blueprint-io.ts +108 -0
  103. package/src/library/blueprint-library-db.ts +112 -0
  104. package/src/library/execution-log-db.ts +171 -0
  105. package/src/library/execution-log-settings.ts +50 -0
  106. package/src/library/swagger-docs.ts +56 -0
  107. package/src/library/types.ts +35 -0
  108. package/src/main.tsx +43 -0
  109. package/src/nodes/AndFlowNode.tsx +59 -0
  110. package/src/nodes/BlueprintFlowNode.tsx +26 -0
  111. package/src/nodes/BlueprintNodeCard.tsx +169 -0
  112. package/src/nodes/BlueprintNodeShell.tsx +45 -0
  113. package/src/nodes/ClockFlowNode.tsx +67 -0
  114. package/src/nodes/FetchFlowNode.tsx +26 -0
  115. package/src/nodes/JsonFlowNode.tsx +26 -0
  116. package/src/nodes/LifecycleFlowNode.tsx +44 -0
  117. package/src/nodes/LogicFlowNode.tsx +26 -0
  118. package/src/runtime/document-to-runnable-graph.ts +48 -0
  119. package/src/runtime/execution-overlay.ts +169 -0
  120. package/src/types.ts +1 -0
  121. package/src/vite-env.d.ts +1 -0
  122. package/tailwind.config.ts +6 -0
  123. package/tsconfig.json +12 -0
  124. package/vite.config.ts +16 -0
@@ -0,0 +1,169 @@
1
+ import { cn } from "@arronqzy/ui";
2
+
3
+ export type BlueprintNodeCardProps = {
4
+ nodeId: string;
5
+ label: string;
6
+ meta?: string;
7
+ /** 节点关键配置摘要,显示在名称下方 */
8
+ subtitle?: string;
9
+ /** 调试执行进度(如时钟 2/3) */
10
+ progressLabel?: string;
11
+ variant?: "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "clock";
12
+ selected?: boolean;
13
+ hideLeadingDot?: boolean;
14
+ onSelect?: (nodeId: string) => void;
15
+ };
16
+
17
+ const variantStyle = {
18
+ blueprint: {
19
+ accent: "border-l-primary",
20
+ badge: "bg-primary/10 text-primary",
21
+ dot: "bg-primary",
22
+ },
23
+ logic: {
24
+ accent: "border-l-sky-500 dark:border-l-sky-400",
25
+ badge: "bg-sky-500/10 text-sky-700 dark:text-sky-300",
26
+ dot: "bg-sky-500",
27
+ },
28
+ and: {
29
+ accent: "border-l-indigo-500 dark:border-l-indigo-400",
30
+ badge: "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300",
31
+ dot: "bg-indigo-500",
32
+ },
33
+ lifecycle: {
34
+ accent: "border-l-amber-500 dark:border-l-amber-400",
35
+ badge: "bg-amber-500/10 text-amber-700 dark:text-amber-300",
36
+ dot: "bg-amber-500",
37
+ },
38
+ fetch: {
39
+ accent: "border-l-violet-500 dark:border-l-violet-400",
40
+ badge: "bg-violet-500/10 text-violet-700 dark:text-violet-300",
41
+ dot: "bg-violet-500",
42
+ },
43
+ json: {
44
+ accent: "border-l-teal-500 dark:border-l-teal-400",
45
+ badge: "bg-teal-500/10 text-teal-700 dark:text-teal-300",
46
+ dot: "bg-teal-500",
47
+ },
48
+ clock: {
49
+ accent: "border-l-rose-500 dark:border-l-rose-400",
50
+ badge: "bg-rose-500/10 text-rose-700 dark:text-rose-300",
51
+ dot: "bg-rose-500",
52
+ },
53
+ } as const;
54
+
55
+ function NodeGrip() {
56
+ return (
57
+ <div
58
+ className="flex shrink-0 flex-col gap-[3px] opacity-35"
59
+ aria-hidden="true"
60
+ >
61
+ <span className="flex gap-[3px]">
62
+ <span className="h-[3px] w-[3px] rounded-full bg-current" />
63
+ <span className="h-[3px] w-[3px] rounded-full bg-current" />
64
+ </span>
65
+ <span className="flex gap-[3px]">
66
+ <span className="h-[3px] w-[3px] rounded-full bg-current" />
67
+ <span className="h-[3px] w-[3px] rounded-full bg-current" />
68
+ </span>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ export function BlueprintNodeCard({
74
+ nodeId,
75
+ label,
76
+ meta,
77
+ subtitle,
78
+ progressLabel,
79
+ variant = "blueprint",
80
+ selected = false,
81
+ hideLeadingDot = false,
82
+ onSelect,
83
+ }: BlueprintNodeCardProps) {
84
+ const v = variantStyle[variant];
85
+
86
+ return (
87
+ <div
88
+ data-blueprint-node-card
89
+ className={cn(
90
+ "bp-node-card w-[168px] overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-sm",
91
+ "transition-[box-shadow,border-color] duration-150",
92
+ hideLeadingDot
93
+ ? "border-t-[3px] border-t-amber-500 dark:border-t-amber-400"
94
+ : cn("border-l-[3px]", v.accent),
95
+ selected &&
96
+ "border-primary/50 shadow-[0_0_0_1px_hsl(var(--primary)/0.35),0_4px_12px_hsl(var(--primary)/0.12)]"
97
+ )}
98
+ >
99
+ <div
100
+ className={cn(
101
+ "bp-flow-drag-handle flex cursor-grab items-center gap-2 border-b border-border/50 px-2 py-1.5",
102
+ "bg-muted/25 text-muted-foreground active:cursor-grabbing"
103
+ )}
104
+ title="拖拽移动"
105
+ >
106
+ <NodeGrip />
107
+ {meta ? (
108
+ <span
109
+ className={cn(
110
+ "min-w-0 flex-1 truncate rounded px-1.5 py-0.5 text-[10px] font-medium leading-none tracking-wide",
111
+ v.badge
112
+ )}
113
+ title={meta}
114
+ >
115
+ {meta}
116
+ </span>
117
+ ) : null}
118
+ </div>
119
+
120
+ <button
121
+ type="button"
122
+ className={cn(
123
+ "nodrag flex w-full text-left outline-none transition-colors hover:bg-accent/30",
124
+ "focus-visible:ring-1 focus-visible:ring-primary focus-visible:ring-inset",
125
+ hideLeadingDot
126
+ ? cn("px-2.5 py-2", subtitle ? "items-start" : "items-center")
127
+ : "items-start gap-2 px-2.5 py-2"
128
+ )}
129
+ onClick={(e) => {
130
+ e.stopPropagation();
131
+ onSelect?.(nodeId);
132
+ }}
133
+ >
134
+ {!hideLeadingDot ? (
135
+ <span
136
+ className={cn("mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full", v.dot)}
137
+ aria-hidden="true"
138
+ />
139
+ ) : null}
140
+ <div className="min-w-0 flex-1">
141
+ <div className="flex items-start gap-1.5">
142
+ <div className="min-w-0 flex-1 truncate text-[13px] font-medium leading-snug text-foreground">
143
+ {label}
144
+ </div>
145
+ {progressLabel ? (
146
+ <span
147
+ className={cn(
148
+ "shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold leading-none",
149
+ v.badge
150
+ )}
151
+ title="已发送信号次数"
152
+ >
153
+ {progressLabel}
154
+ </span>
155
+ ) : null}
156
+ </div>
157
+ {subtitle ? (
158
+ <div
159
+ className="mt-0.5 truncate font-mono text-[10px] leading-tight text-muted-foreground"
160
+ title={subtitle}
161
+ >
162
+ {subtitle}
163
+ </div>
164
+ ) : null}
165
+ </div>
166
+ </button>
167
+ </div>
168
+ );
169
+ }
@@ -0,0 +1,45 @@
1
+ import { Handle, Position } from "@xyflow/react";
2
+ import { cn } from "@arronqzy/ui";
3
+
4
+ import type { BlueprintNodeExecutionTone } from "../runtime/execution-overlay";
5
+ import { BlueprintNodeCard, type BlueprintNodeCardProps } from "./BlueprintNodeCard";
6
+
7
+ export type BlueprintNodeShellProps = BlueprintNodeCardProps & {
8
+ /** 用户在画布上点选的配置节点 */
9
+ selected?: boolean;
10
+ /** 调试执行中当前节点的信号高亮(与用户选中独立) */
11
+ executionTone?: BlueprintNodeExecutionTone | null;
12
+ };
13
+
14
+ /** 左进右出的标准节点连线布局 */
15
+ export function BlueprintNodeShell({
16
+ selected = false,
17
+ executionTone = null,
18
+ ...cardProps
19
+ }: BlueprintNodeShellProps) {
20
+ return (
21
+ <div
22
+ className={cn(
23
+ "bp-node",
24
+ executionTone === "success" && "bp-node--execution-true",
25
+ executionTone === "error" && "bp-node--execution-false"
26
+ )}
27
+ >
28
+ <Handle
29
+ type="target"
30
+ position={Position.Left}
31
+ id="in"
32
+ className="bp-flow-handle bp-flow-handle--target"
33
+ title="真/假信号输入"
34
+ />
35
+ <BlueprintNodeCard {...cardProps} selected={selected} />
36
+ <Handle
37
+ type="source"
38
+ position={Position.Right}
39
+ id="out"
40
+ className="bp-flow-handle bp-flow-handle--source"
41
+ title="真/假信号输出"
42
+ />
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,67 @@
1
+ import type { NodeProps } from "@xyflow/react";
2
+ import { cn } from "@arronqzy/ui";
3
+
4
+ import { resolveBlueprintNodeTypeLabel } from "../graph/document";
5
+ import { resolveBlueprintNodeSummary } from "../graph/node-summary";
6
+ import {
7
+ useBlueprintClockAbort,
8
+ useBlueprintNodeSelect,
9
+ useClockNodeCanAbort,
10
+ } from "../BlueprintCanvasContext";
11
+ import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
12
+ import type { BlueprintFlowNodeData } from "../types";
13
+ import { BlueprintNodeShell } from "./BlueprintNodeShell";
14
+
15
+ function ClockAbortIcon({ className }: { className?: string }) {
16
+ return (
17
+ <svg viewBox="0 0 24 24" className={className} aria-hidden>
18
+ <rect x="6" y="6" width="12" height="12" rx="1.5" fill="currentColor" />
19
+ </svg>
20
+ );
21
+ }
22
+
23
+ /** 时钟节点:收到真信号后按配置间隔输出含当前时间的真信号 */
24
+ export function ClockFlowNode({ id, data }: NodeProps) {
25
+ const nodeData = data as BlueprintFlowNodeData;
26
+ const onSelect = useBlueprintNodeSelect();
27
+ const onAbortClock = useBlueprintClockAbort();
28
+ const canAbort = useClockNodeCanAbort(id);
29
+ const progress = nodeData.clockEmitProgress;
30
+ const progressLabel = progress
31
+ ? `${progress.current}/${progress.total}`
32
+ : undefined;
33
+
34
+ return (
35
+ <div className="relative">
36
+ <BlueprintNodeShell
37
+ nodeId={id}
38
+ label={nodeData.label}
39
+ meta={resolveBlueprintNodeTypeLabel(nodeData)}
40
+ subtitle={resolveBlueprintNodeSummary(nodeData)}
41
+ progressLabel={progressLabel}
42
+ variant="clock"
43
+ selected={Boolean(nodeData.isSelected)}
44
+ executionTone={resolveBlueprintNodeExecutionTone(nodeData)}
45
+ onSelect={onSelect}
46
+ />
47
+ {canAbort ? (
48
+ <button
49
+ type="button"
50
+ className={cn(
51
+ "nodrag nopan absolute right-1.5 top-1.5 z-10 flex h-5 w-5 items-center justify-center",
52
+ "rounded border border-rose-500/50 bg-rose-500/15 text-rose-600 shadow-sm",
53
+ "hover:bg-rose-500/25 dark:text-rose-300"
54
+ )}
55
+ title="中止时钟:停止剩余次数与正在执行的下游任务"
56
+ onPointerDown={(e) => e.stopPropagation()}
57
+ onClick={(e) => {
58
+ e.stopPropagation();
59
+ onAbortClock(id);
60
+ }}
61
+ >
62
+ <ClockAbortIcon className="h-3 w-3" />
63
+ </button>
64
+ ) : null}
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,26 @@
1
+ import type { NodeProps } from "@xyflow/react";
2
+
3
+ import { resolveBlueprintNodeTypeLabel } from "../graph/document";
4
+ import { resolveBlueprintNodeSummary } from "../graph/node-summary";
5
+ import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
6
+ import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
7
+ import type { BlueprintFlowNodeData } from "../types";
8
+ import { BlueprintNodeShell } from "./BlueprintNodeShell";
9
+
10
+ export function FetchFlowNode({ id, data }: NodeProps) {
11
+ const nodeData = data as BlueprintFlowNodeData;
12
+ const onSelect = useBlueprintNodeSelect();
13
+
14
+ return (
15
+ <BlueprintNodeShell
16
+ nodeId={id}
17
+ label={nodeData.label}
18
+ meta={resolveBlueprintNodeTypeLabel(nodeData)}
19
+ subtitle={resolveBlueprintNodeSummary(nodeData)}
20
+ variant="fetch"
21
+ selected={Boolean(nodeData.isSelected)}
22
+ executionTone={resolveBlueprintNodeExecutionTone(nodeData)}
23
+ onSelect={onSelect}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,26 @@
1
+ import type { NodeProps } from "@xyflow/react";
2
+
3
+ import { resolveBlueprintNodeTypeLabel } from "../graph/document";
4
+ import { resolveBlueprintNodeSummary } from "../graph/node-summary";
5
+ import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
6
+ import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
7
+ import type { BlueprintFlowNodeData } from "../types";
8
+ import { BlueprintNodeShell } from "./BlueprintNodeShell";
9
+
10
+ export function JsonFlowNode({ id, data }: NodeProps) {
11
+ const nodeData = data as BlueprintFlowNodeData;
12
+ const onSelect = useBlueprintNodeSelect();
13
+
14
+ return (
15
+ <BlueprintNodeShell
16
+ nodeId={id}
17
+ label={nodeData.label}
18
+ meta={resolveBlueprintNodeTypeLabel(nodeData)}
19
+ subtitle={resolveBlueprintNodeSummary(nodeData)}
20
+ variant="json"
21
+ selected={Boolean(nodeData.isSelected)}
22
+ executionTone={resolveBlueprintNodeExecutionTone(nodeData)}
23
+ onSelect={onSelect}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,44 @@
1
+ import { Handle, Position, type NodeProps } from "@xyflow/react";
2
+ import { cn } from "@arronqzy/ui";
3
+
4
+ import { resolveBlueprintNodeTypeLabel } from "../graph/document";
5
+ import { resolveBlueprintNodeSummary } from "../graph/node-summary";
6
+ import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
7
+ import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
8
+ import type { BlueprintFlowNodeData } from "../types";
9
+ import { BlueprintNodeCard } from "./BlueprintNodeCard";
10
+
11
+ /** 生命周期节点:无输入口,仅在对应生命周期触发时从右侧输出口发出信号 */
12
+ export function LifecycleFlowNode({ id, data }: NodeProps) {
13
+ const nodeData = data as BlueprintFlowNodeData;
14
+ const onSelect = useBlueprintNodeSelect();
15
+ const executionTone = resolveBlueprintNodeExecutionTone(nodeData);
16
+
17
+ return (
18
+ <div
19
+ className={cn(
20
+ "bp-node bp-node--lifecycle",
21
+ executionTone === "success" && "bp-node--execution-true",
22
+ executionTone === "error" && "bp-node--execution-false"
23
+ )}
24
+ >
25
+ <BlueprintNodeCard
26
+ nodeId={id}
27
+ label={nodeData.label}
28
+ meta={resolveBlueprintNodeTypeLabel(nodeData)}
29
+ subtitle={resolveBlueprintNodeSummary(nodeData)}
30
+ variant="lifecycle"
31
+ selected={Boolean(nodeData.isSelected)}
32
+ hideLeadingDot
33
+ onSelect={onSelect}
34
+ />
35
+ <Handle
36
+ type="source"
37
+ position={Position.Right}
38
+ id="out"
39
+ className="bp-flow-handle bp-flow-handle--source"
40
+ title="生命周期满足时输出真/假信号"
41
+ />
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,26 @@
1
+ import type { NodeProps } from "@xyflow/react";
2
+
3
+ import { resolveBlueprintNodeTypeLabel } from "../graph/document";
4
+ import { resolveBlueprintNodeSummary } from "../graph/node-summary";
5
+ import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
6
+ import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
7
+ import type { BlueprintFlowNodeData } from "../types";
8
+ import { BlueprintNodeShell } from "./BlueprintNodeShell";
9
+
10
+ export function LogicFlowNode({ id, data }: NodeProps) {
11
+ const nodeData = data as BlueprintFlowNodeData;
12
+ const onSelect = useBlueprintNodeSelect();
13
+
14
+ return (
15
+ <BlueprintNodeShell
16
+ nodeId={id}
17
+ label={nodeData.label}
18
+ meta={resolveBlueprintNodeTypeLabel(nodeData)}
19
+ subtitle={resolveBlueprintNodeSummary(nodeData)}
20
+ variant="logic"
21
+ selected={Boolean(nodeData.isSelected)}
22
+ executionTone={resolveBlueprintNodeExecutionTone(nodeData)}
23
+ onSelect={onSelect}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ VIEW_NODE_TYPE,
3
+ type RunnableGraph,
4
+ } from "@arronqzy/blueprint-dsl";
5
+
6
+ import {
7
+ resolveBlueprintConfigSource,
8
+ resolveViewElementIds,
9
+ type BlueprintDocument,
10
+ } from "../graph/document";
11
+
12
+ export function documentToRunnableGraph(
13
+ document: BlueprintDocument,
14
+ options?: {
15
+ libraryNameById?: ReadonlyMap<string, string>;
16
+ }
17
+ ): RunnableGraph {
18
+ return {
19
+ nodes: document.nodes.map((node) => {
20
+ const configSource = resolveBlueprintConfigSource(node);
21
+ const viewElementIds = resolveViewElementIds(node);
22
+ return {
23
+ id: node.id,
24
+ nodeType:
25
+ configSource === "view" ? VIEW_NODE_TYPE : node.nodeType,
26
+ lifecyclePhase: node.lifecyclePhase,
27
+ libraryBlueprintId: node.libraryBlueprintId,
28
+ viewElementIds:
29
+ viewElementIds.length > 0 ? viewElementIds : undefined,
30
+ fetchConfig: node.fetchConfig,
31
+ jsonConfig: node.jsonConfig,
32
+ logicConfig: node.logicConfig,
33
+ clockConfig: node.clockConfig,
34
+ blueprintName: node.libraryBlueprintId
35
+ ? (options?.libraryNameById?.get(node.libraryBlueprintId) ??
36
+ node.label)
37
+ : undefined,
38
+ };
39
+ }),
40
+ edges: document.edges.map((edge) => ({
41
+ source: edge.source,
42
+ target: edge.target,
43
+ sourceHandle: edge.sourceHandle,
44
+ targetHandle: edge.targetHandle,
45
+ })),
46
+ allowFalseSignalPropagation: document.allowFalseSignalPropagation,
47
+ };
48
+ }
@@ -0,0 +1,169 @@
1
+ import {
2
+ CLOCK_NODE_TYPE,
3
+ type ExecutionTraceEntry,
4
+ } from "@arronqzy/blueprint-dsl";
5
+
6
+ import type { BlueprintGraph } from "../graph/blueprint-graph";
7
+
8
+ export type BlueprintNodeSignalKind = "true" | "false";
9
+
10
+ export type BlueprintEdgeSignalKind = BlueprintNodeSignalKind | "clock-green" | "clock-blue";
11
+
12
+ export type BlueprintExecutionOverlay = {
13
+ /** 当前执行到的节点(最后一条 trace 对应节点) */
14
+ activeNodeId: string | null;
15
+ /** 当前执行节点的主输出信号类型 */
16
+ activeNodeSignalKind: BlueprintNodeSignalKind | null;
17
+ /** 边 id -> 该边最近一次流出的信号类型 */
18
+ edgeSignals: Record<string, BlueprintEdgeSignalKind>;
19
+ /** 时钟节点输出口连线对应当前 tick(用于同色系再次输出时触发重置动画) */
20
+ clockEdgeTicks?: Record<string, number>;
21
+ /** 时钟节点已发送信号进度(调试用) */
22
+ clockNodeProgress?: Record<string, { current: number; total: number }>;
23
+ };
24
+
25
+ function readOutputSignalKind(value: unknown): BlueprintNodeSignalKind | null {
26
+ if (!value || typeof value !== "object" || !("kind" in value)) return null;
27
+ const kind = (value as { kind: unknown }).kind;
28
+ if (kind === "true" || kind === "false") return kind;
29
+ return null;
30
+ }
31
+
32
+ function resolveOutputEdgeSignalKind(
33
+ entry: ExecutionTraceEntry,
34
+ value: unknown
35
+ ): BlueprintEdgeSignalKind | null {
36
+ const kind = readOutputSignalKind(value);
37
+ if (!kind) return null;
38
+ // 时钟每次输出真信号时连线均显示为绿色(与其它真信号一致)
39
+ if (entry.nodeType === CLOCK_NODE_TYPE && kind === "true") {
40
+ return "true";
41
+ }
42
+ return kind;
43
+ }
44
+
45
+ function applyClockEdgeTicks(
46
+ graph: BlueprintGraph,
47
+ entry: ExecutionTraceEntry,
48
+ clockEdgeTicks: Record<string, number>
49
+ ) {
50
+ const tickIndex = entry.clockTickIndex ?? 1;
51
+ for (const edge of graph.document.edges) {
52
+ if (
53
+ edge.source === entry.nodeId &&
54
+ (edge.sourceHandle ?? "out") === "out"
55
+ ) {
56
+ clockEdgeTicks[edge.id] = tickIndex;
57
+ }
58
+ }
59
+ }
60
+
61
+ function findLastClockTraceEntry(
62
+ entries: ExecutionTraceEntry[]
63
+ ): ExecutionTraceEntry | undefined {
64
+ return [...entries]
65
+ .reverse()
66
+ .find(
67
+ (entry) =>
68
+ entry.nodeType === CLOCK_NODE_TYPE &&
69
+ getTraceEntryOutputKind(entry) === "true"
70
+ );
71
+ }
72
+
73
+ function applyEntryOutputEdgeSignals(
74
+ graph: BlueprintGraph,
75
+ entry: ExecutionTraceEntry,
76
+ edgeSignals: Record<string, BlueprintEdgeSignalKind>
77
+ ) {
78
+ for (const [port, value] of Object.entries(entry.outputs)) {
79
+ const kind = resolveOutputEdgeSignalKind(entry, value);
80
+ if (!kind) continue;
81
+
82
+ for (const edge of graph.document.edges) {
83
+ if (
84
+ edge.source === entry.nodeId &&
85
+ (edge.sourceHandle ?? "out") === port
86
+ ) {
87
+ edgeSignals[edge.id] = kind;
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ export function getTraceEntryOutputKind(
94
+ entry: ExecutionTraceEntry,
95
+ port = "out"
96
+ ): BlueprintNodeSignalKind | null {
97
+ if (entry.error) return "false";
98
+ return readOutputSignalKind(entry.outputs[port]);
99
+ }
100
+
101
+ export function isTraceHaltedByFalseSignal(
102
+ entries: ExecutionTraceEntry[]
103
+ ): boolean {
104
+ if (entries.length === 0) return false;
105
+ return getTraceEntryOutputKind(entries[entries.length - 1]!) === "false";
106
+ }
107
+
108
+ export function shouldHaltDebugOnFalseSignal(
109
+ entries: ExecutionTraceEntry[],
110
+ allowFalseSignalPropagation = false
111
+ ): boolean {
112
+ if (allowFalseSignalPropagation) return false;
113
+ return isTraceHaltedByFalseSignal(entries);
114
+ }
115
+
116
+ export function buildExecutionOverlay(
117
+ graph: BlueprintGraph,
118
+ entries: ExecutionTraceEntry[]
119
+ ): BlueprintExecutionOverlay | null {
120
+ if (entries.length === 0) return null;
121
+
122
+ const lastEntry = entries[entries.length - 1]!;
123
+ const edgeSignals: Record<string, BlueprintEdgeSignalKind> = {};
124
+ const clockEdgeTicks: Record<string, number> = {};
125
+
126
+ // 按执行顺序累积各输出口最近一次信号;未再次输出的连线保持变色
127
+ for (const entry of entries) {
128
+ applyEntryOutputEdgeSignals(graph, entry, edgeSignals);
129
+ if (
130
+ entry.nodeType === CLOCK_NODE_TYPE &&
131
+ getTraceEntryOutputKind(entry) === "true"
132
+ ) {
133
+ applyClockEdgeTicks(graph, entry, clockEdgeTicks);
134
+ }
135
+ }
136
+
137
+ const lastClockEntry = findLastClockTraceEntry(entries);
138
+
139
+ const clockNodeProgress: Record<string, { current: number; total: number }> =
140
+ {};
141
+ if (lastClockEntry) {
142
+ clockNodeProgress[lastClockEntry.nodeId] = {
143
+ current: lastClockEntry.clockTickIndex ?? 1,
144
+ total: lastClockEntry.clockTickTotal ?? 1,
145
+ };
146
+ }
147
+
148
+ return {
149
+ activeNodeId: lastEntry.nodeId,
150
+ activeNodeSignalKind: getTraceEntryOutputKind(lastEntry),
151
+ edgeSignals,
152
+ clockEdgeTicks:
153
+ Object.keys(clockEdgeTicks).length > 0 ? clockEdgeTicks : undefined,
154
+ clockNodeProgress:
155
+ Object.keys(clockNodeProgress).length > 0 ? clockNodeProgress : undefined,
156
+ };
157
+ }
158
+
159
+ export type BlueprintNodeExecutionTone = "success" | "error";
160
+
161
+ export function resolveBlueprintNodeExecutionTone(data: {
162
+ isExecutionActive?: boolean;
163
+ executionSignalKind?: BlueprintNodeSignalKind | null;
164
+ }): BlueprintNodeExecutionTone | undefined {
165
+ if (!data.isExecutionActive) return undefined;
166
+ if (data.executionSignalKind === "false") return "error";
167
+ if (data.executionSignalKind === "true") return "success";
168
+ return undefined;
169
+ }
package/src/types.ts ADDED
@@ -0,0 +1 @@
1
+ export type { BlueprintFlowNodeData } from "./graph/react-flow-adapter";
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,6 @@
1
+ import preset from "../tailwind/src/preset";
2
+
3
+ export default {
4
+ presets: [preset],
5
+ content: ["./index.html", "./src/**/*.{ts,tsx}"],
6
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "@arronqzy/typescript-config/react-library.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "@arronqzy/blueprint-dsl": ["../blueprint-dsl/src/index.ts"],
7
+ "@arronqzy/ui": ["../ui/index.ts"]
8
+ }
9
+ },
10
+ "include": ["."],
11
+ "exclude": ["node_modules"]
12
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,16 @@
1
+ import path from "node:path";
2
+
3
+ import react from "@vitejs/plugin-react";
4
+ import { defineConfig } from "vite";
5
+
6
+ export default defineConfig({
7
+ plugins: [react()],
8
+ resolve: {
9
+ alias: {
10
+ "@arronqzy/ui": path.resolve(process.cwd(), "../ui/index.ts"),
11
+ },
12
+ },
13
+ server: {
14
+ port: 31012,
15
+ },
16
+ });