@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,664 @@
1
+ import { useEffect, useState, type ReactElement } from "react";
2
+ import {
3
+ Button,
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ Input,
11
+ Label,
12
+ Tooltip,
13
+ TooltipContent,
14
+ TooltipProvider,
15
+ TooltipTrigger,
16
+ } from "@arronqzy/ui";
17
+
18
+ import type { BlueprintLibraryListItem } from "../library/types";
19
+ import type { LifecycleNodeOption } from "../hooks/useBlueprintDebugSession";
20
+ import { PAGE_LIFECYCLE_LABELS, type PageLifecyclePhase } from "@arronqzy/blueprint-dsl";
21
+
22
+ export type BlueprintRenameDialogProps = {
23
+ open: boolean;
24
+ initialName: string;
25
+ onOpenChange: (open: boolean) => void;
26
+ onConfirm: (name: string) => void;
27
+ };
28
+
29
+ export function BlueprintRenameDialog({
30
+ open,
31
+ initialName,
32
+ onOpenChange,
33
+ onConfirm,
34
+ }: BlueprintRenameDialogProps) {
35
+ const [name, setName] = useState(initialName);
36
+
37
+ useEffect(() => {
38
+ if (!open) return;
39
+ setName(initialName);
40
+ }, [initialName, open]);
41
+
42
+ const handleConfirm = () => {
43
+ const next = name.trim();
44
+ if (!next) return;
45
+ onConfirm(next);
46
+ onOpenChange(false);
47
+ };
48
+
49
+ return (
50
+ <Dialog open={open} onOpenChange={onOpenChange}>
51
+ <DialogContent className="sm:max-w-sm">
52
+ <DialogHeader>
53
+ <DialogTitle>重命名蓝图</DialogTitle>
54
+ <DialogDescription>修改蓝图库中该项的显示名称。</DialogDescription>
55
+ </DialogHeader>
56
+ <div className="space-y-1.5 py-1">
57
+ <Label htmlFor="blueprint-rename-input">蓝图名称</Label>
58
+ <Input
59
+ id="blueprint-rename-input"
60
+ value={name}
61
+ onChange={(e) => setName(e.target.value)}
62
+ onKeyDown={(e) => {
63
+ if (e.key === "Enter") handleConfirm();
64
+ }}
65
+ autoFocus
66
+ />
67
+ </div>
68
+ <DialogFooter>
69
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
70
+ 取消
71
+ </Button>
72
+ <Button type="button" onClick={handleConfirm} disabled={!name.trim()}>
73
+ 确定
74
+ </Button>
75
+ </DialogFooter>
76
+ </DialogContent>
77
+ </Dialog>
78
+ );
79
+ }
80
+
81
+ function IconSave() {
82
+ return (
83
+ <svg
84
+ viewBox="0 0 24 24"
85
+ className="h-3.5 w-3.5"
86
+ fill="none"
87
+ stroke="currentColor"
88
+ strokeWidth="2"
89
+ aria-hidden="true"
90
+ >
91
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
92
+ <path d="M17 21v-8H7v8M7 3v5h8" />
93
+ </svg>
94
+ );
95
+ }
96
+
97
+ function IconSync() {
98
+ return (
99
+ <svg
100
+ viewBox="0 0 24 24"
101
+ className="h-3.5 w-3.5"
102
+ fill="none"
103
+ stroke="currentColor"
104
+ strokeWidth="2"
105
+ aria-hidden="true"
106
+ >
107
+ <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
108
+ <path d="M3 3v5h5" />
109
+ <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
110
+ <path d="M16 16h5v5" />
111
+ </svg>
112
+ );
113
+ }
114
+
115
+ function IconPencil() {
116
+ return (
117
+ <svg
118
+ viewBox="0 0 24 24"
119
+ className="h-3 w-3"
120
+ fill="none"
121
+ stroke="currentColor"
122
+ strokeWidth="2"
123
+ aria-hidden="true"
124
+ >
125
+ <path d="M12 20h9" />
126
+ <path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
127
+ </svg>
128
+ );
129
+ }
130
+
131
+ function IconTrash() {
132
+ return (
133
+ <svg
134
+ viewBox="0 0 24 24"
135
+ className="h-3 w-3"
136
+ fill="none"
137
+ stroke="currentColor"
138
+ strokeWidth="2"
139
+ aria-hidden="true"
140
+ >
141
+ <path d="M3 6h18" />
142
+ <path d="M8 6V4h8v2" />
143
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
144
+ <path d="M10 11v6M14 11v6" />
145
+ </svg>
146
+ );
147
+ }
148
+
149
+ function IconChevronDown() {
150
+ return (
151
+ <svg
152
+ viewBox="0 0 24 24"
153
+ className="h-3.5 w-3.5 shrink-0 opacity-60"
154
+ fill="none"
155
+ stroke="currentColor"
156
+ strokeWidth="2"
157
+ aria-hidden="true"
158
+ >
159
+ <path d="m6 9 6 6 6-6" />
160
+ </svg>
161
+ );
162
+ }
163
+
164
+ export type BlueprintDeleteDialogProps = {
165
+ open: boolean;
166
+ blueprintName: string;
167
+ onOpenChange: (open: boolean) => void;
168
+ onConfirm: () => void;
169
+ };
170
+
171
+ export function BlueprintDeleteDialog({
172
+ open,
173
+ blueprintName,
174
+ onOpenChange,
175
+ onConfirm,
176
+ }: BlueprintDeleteDialogProps) {
177
+ return (
178
+ <Dialog open={open} onOpenChange={onOpenChange}>
179
+ <DialogContent className="sm:max-w-sm">
180
+ <DialogHeader>
181
+ <DialogTitle>删除蓝图</DialogTitle>
182
+ <DialogDescription>
183
+ 确定从蓝图库中删除「{blueprintName}」吗?此操作不可恢复。
184
+ </DialogDescription>
185
+ </DialogHeader>
186
+ <DialogFooter>
187
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
188
+ 取消
189
+ </Button>
190
+ <Button
191
+ type="button"
192
+ variant="destructive"
193
+ onClick={() => {
194
+ onConfirm();
195
+ onOpenChange(false);
196
+ }}
197
+ >
198
+ 删除
199
+ </Button>
200
+ </DialogFooter>
201
+ </DialogContent>
202
+ </Dialog>
203
+ );
204
+ }
205
+
206
+ function IconLog({ className }: { className?: string }) {
207
+ return (
208
+ <svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
209
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
210
+ <path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
211
+ </svg>
212
+ );
213
+ }
214
+
215
+ /** 快进:双三角,表示走完全流程 */
216
+ function IconRunAll({ className }: { className?: string }) {
217
+ return (
218
+ <svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden="true">
219
+ <path d="M5 5v14l7-7-7-7z" />
220
+ <path d="M12 5v14l7-7-7-7z" />
221
+ </svg>
222
+ );
223
+ }
224
+
225
+ /** 单三角播放:表示单步执行下一步 */
226
+ function IconStepNext({ className }: { className?: string }) {
227
+ return (
228
+ <svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden="true">
229
+ <path d="M6 4v16l14-8-14-8z" />
230
+ </svg>
231
+ );
232
+ }
233
+
234
+ /** 左向三角:回到上一步 */
235
+ function IconStepBack({ className }: { className?: string }) {
236
+ return (
237
+ <svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden="true">
238
+ <path d="M18 4v16l-14-8 14-8z" />
239
+ </svg>
240
+ );
241
+ }
242
+
243
+ /** 回到开始:竖线 + 左向三角 */
244
+ function IconResetToStart({ className }: { className?: string }) {
245
+ return (
246
+ <svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden="true">
247
+ <path d="M5 5v14" stroke="currentColor" strokeWidth="2" fill="none" />
248
+ <path d="M20 4v16l-14-8 14-8z" />
249
+ </svg>
250
+ );
251
+ }
252
+
253
+ export type BlueprintDebugToolbarProps = {
254
+ lifecycleNodes: LifecycleNodeOption[];
255
+ selectedLifecycleNodeId: string | null;
256
+ onSelectLifecycleNode: (id: string | null) => void;
257
+ onRunAll: () => void;
258
+ onResetToStart: () => void;
259
+ onStepBack: () => void;
260
+ onStepNext: () => void;
261
+ canResetToStart?: boolean;
262
+ canStepBack?: boolean;
263
+ canStepNext?: boolean;
264
+ chainComplete?: boolean;
265
+ falseSignalHalt?: boolean;
266
+ logPanelOpen: boolean;
267
+ onToggleLogPanel: () => void;
268
+ running?: boolean;
269
+ };
270
+
271
+ export type BlueprintPanelToolbarProps = {
272
+ items: BlueprintLibraryListItem[];
273
+ activeId: string | null;
274
+ /** 未选中库蓝图时,下拉触发器展示的工作区蓝图名称 */
275
+ currentBlueprintLabel?: string;
276
+ onSelectItem: (id: string) => void;
277
+ onRenameItem: (id: string, name: string) => void;
278
+ onDeleteItem: (id: string) => void;
279
+ onSave: () => void;
280
+ onSync?: () => void;
281
+ canSync?: boolean;
282
+ debug?: BlueprintDebugToolbarProps;
283
+ };
284
+
285
+ function groupItems(items: BlueprintLibraryListItem[]) {
286
+ return {
287
+ saved: items.filter((item) => item.source === "saved"),
288
+ imported: items.filter((item) => item.source === "imported"),
289
+ };
290
+ }
291
+
292
+ function ToolbarTooltip({
293
+ content,
294
+ children,
295
+ }: {
296
+ content: string;
297
+ children: ReactElement;
298
+ }) {
299
+ return (
300
+ <Tooltip>
301
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
302
+ <TooltipContent side="bottom" className="max-w-[260px] text-[11px]">
303
+ {content}
304
+ </TooltipContent>
305
+ </Tooltip>
306
+ );
307
+ }
308
+
309
+ function ToolbarTooltipButton({
310
+ content,
311
+ disabled,
312
+ className,
313
+ ...props
314
+ }: React.ComponentProps<typeof Button> & { content: string }) {
315
+ const button = (
316
+ <Button disabled={disabled} className={className} {...props} />
317
+ );
318
+ return (
319
+ <ToolbarTooltip content={content}>
320
+ {disabled ? <span className="inline-flex shrink-0">{button}</span> : button}
321
+ </ToolbarTooltip>
322
+ );
323
+ }
324
+
325
+ function LibraryRow({
326
+ item,
327
+ active,
328
+ onSelect,
329
+ onRename,
330
+ onDelete,
331
+ }: {
332
+ item: BlueprintLibraryListItem;
333
+ active: boolean;
334
+ onSelect: () => void;
335
+ onRename: () => void;
336
+ onDelete: () => void;
337
+ }) {
338
+ const nameButton = (
339
+ <button
340
+ type="button"
341
+ className="min-w-0 flex-1 truncate px-2 py-1.5 text-left text-xs hover:bg-accent/60"
342
+ onClick={onSelect}
343
+ >
344
+ {item.name}
345
+ </button>
346
+ );
347
+
348
+ return (
349
+ <div
350
+ className={`flex items-center gap-0.5 rounded-sm pr-0.5 ${
351
+ active ? "bg-accent" : ""
352
+ }`}
353
+ >
354
+ {active ? (
355
+ <ToolbarTooltip content="再次点击返回当前工作区蓝图">
356
+ {nameButton}
357
+ </ToolbarTooltip>
358
+ ) : (
359
+ nameButton
360
+ )}
361
+ <ToolbarTooltip content={`重命名「${item.name}」`}>
362
+ <button
363
+ type="button"
364
+ className="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
365
+ aria-label={`重命名 ${item.name}`}
366
+ onClick={(e) => {
367
+ e.stopPropagation();
368
+ onRename();
369
+ }}
370
+ >
371
+ <IconPencil />
372
+ </button>
373
+ </ToolbarTooltip>
374
+ <ToolbarTooltip content={`从蓝图库删除「${item.name}」`}>
375
+ <button
376
+ type="button"
377
+ className="shrink-0 rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
378
+ aria-label={`删除 ${item.name}`}
379
+ onClick={(e) => {
380
+ e.stopPropagation();
381
+ onDelete();
382
+ }}
383
+ >
384
+ <IconTrash />
385
+ </button>
386
+ </ToolbarTooltip>
387
+ </div>
388
+ );
389
+ }
390
+
391
+ export function BlueprintPanelToolbar({
392
+ items,
393
+ activeId,
394
+ currentBlueprintLabel,
395
+ onSelectItem,
396
+ onRenameItem,
397
+ onDeleteItem,
398
+ onSave,
399
+ onSync,
400
+ canSync = false,
401
+ debug,
402
+ }: BlueprintPanelToolbarProps) {
403
+ const [menuOpen, setMenuOpen] = useState(false);
404
+ const [renameTarget, setRenameTarget] = useState<BlueprintLibraryListItem | null>(
405
+ null
406
+ );
407
+ const [deleteTarget, setDeleteTarget] = useState<BlueprintLibraryListItem | null>(
408
+ null
409
+ );
410
+
411
+ const activeItem = items.find((item) => item.id === activeId) ?? null;
412
+ const { saved, imported } = groupItems(items);
413
+ const triggerLabel =
414
+ activeItem?.name ??
415
+ (currentBlueprintLabel?.trim() ||
416
+ (items.length > 0 ? "蓝图库" : "蓝图库为空"));
417
+
418
+ const handleRenameConfirm = (name: string) => {
419
+ if (!renameTarget) return;
420
+ onRenameItem(renameTarget.id, name);
421
+ setRenameTarget(null);
422
+ };
423
+
424
+ return (
425
+ <>
426
+ <TooltipProvider delayDuration={200}>
427
+ <div className="flex min-w-0 flex-1 items-center gap-1.5">
428
+ <div className="relative shrink-0">
429
+ <ToolbarTooltip content="打开蓝图库:点击项加载;已选中项再次点击可返回当前工作区蓝图">
430
+ <span className={items.length === 0 ? "inline-flex" : undefined}>
431
+ <Button
432
+ type="button"
433
+ variant="outline"
434
+ size="sm"
435
+ className="h-7 w-[148px] justify-between gap-1 px-2 text-xs font-normal"
436
+ disabled={items.length === 0}
437
+ onClick={() => setMenuOpen((open) => !open)}
438
+ >
439
+ <span className="min-w-0 truncate">{triggerLabel}</span>
440
+ <IconChevronDown />
441
+ </Button>
442
+ </span>
443
+ </ToolbarTooltip>
444
+ {menuOpen && items.length > 0 ? (
445
+ <>
446
+ <div
447
+ className="fixed inset-0 z-[10090]"
448
+ onClick={() => setMenuOpen(false)}
449
+ aria-hidden="true"
450
+ />
451
+ <div className="absolute left-0 top-[calc(100%+4px)] z-[10100] w-[240px] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md">
452
+ {saved.length > 0 ? (
453
+ <div className="mb-1">
454
+ <div className="px-2 py-1 text-[10px] font-semibold text-muted-foreground">
455
+ 已保存
456
+ </div>
457
+ {saved.map((item) => (
458
+ <LibraryRow
459
+ key={item.id}
460
+ item={item}
461
+ active={item.id === activeId}
462
+ onSelect={() => {
463
+ onSelectItem(item.id);
464
+ setMenuOpen(false);
465
+ }}
466
+ onRename={() => setRenameTarget(item)}
467
+ onDelete={() => setDeleteTarget(item)}
468
+ />
469
+ ))}
470
+ </div>
471
+ ) : null}
472
+ {imported.length > 0 ? (
473
+ <div>
474
+ <div className="px-2 py-1 text-[10px] font-semibold text-muted-foreground">
475
+ 已导入
476
+ </div>
477
+ {imported.map((item) => (
478
+ <LibraryRow
479
+ key={item.id}
480
+ item={item}
481
+ active={item.id === activeId}
482
+ onSelect={() => {
483
+ onSelectItem(item.id);
484
+ setMenuOpen(false);
485
+ }}
486
+ onRename={() => setRenameTarget(item)}
487
+ onDelete={() => setDeleteTarget(item)}
488
+ />
489
+ ))}
490
+ </div>
491
+ ) : null}
492
+ </div>
493
+ </>
494
+ ) : null}
495
+ </div>
496
+
497
+ <ToolbarTooltipButton
498
+ type="button"
499
+ variant="outline"
500
+ size="icon"
501
+ className="h-7 w-7 shrink-0"
502
+ content="保存蓝图到本地蓝图库"
503
+ aria-label="保存蓝图到本地"
504
+ onClick={onSave}
505
+ >
506
+ <IconSave />
507
+ </ToolbarTooltipButton>
508
+
509
+ {onSync ? (
510
+ <ToolbarTooltipButton
511
+ type="button"
512
+ variant={canSync ? "default" : "outline"}
513
+ size="icon"
514
+ className="h-7 w-7 shrink-0"
515
+ content={
516
+ canSync
517
+ ? activeItem
518
+ ? `同步更新蓝图库中的「${activeItem.name}」`
519
+ : "将当前编辑同步到蓝图库"
520
+ : activeItem
521
+ ? `「${activeItem.name}」已与库中版本一致`
522
+ : "请先从蓝图库选择要编辑的蓝图"
523
+ }
524
+ aria-label="同步蓝图到库"
525
+ disabled={!canSync}
526
+ onClick={onSync}
527
+ >
528
+ <IconSync />
529
+ </ToolbarTooltipButton>
530
+ ) : null}
531
+
532
+ {debug && debug.lifecycleNodes.length > 0 ? (
533
+ <>
534
+ <ToolbarTooltip content="模拟场景:选择生命周期节点,表示当前处于该生命周期阶段">
535
+ <select
536
+ value={debug.selectedLifecycleNodeId ?? ""}
537
+ onChange={(e) => debug.onSelectLifecycleNode(e.target.value || null)}
538
+ className="h-7 max-w-[180px] shrink-0 rounded-md border border-input bg-background px-2 text-[11px] text-foreground outline-none focus-visible:ring-1 focus-visible:ring-primary"
539
+ aria-label="模拟场景"
540
+ >
541
+ <option value="">模拟场景</option>
542
+ {debug.lifecycleNodes.map((node) => (
543
+ <option key={node.id} value={node.id}>
544
+ {node.label}
545
+ {node.phase
546
+ ? ` · ${PAGE_LIFECYCLE_LABELS[node.phase as PageLifecyclePhase] ?? node.phase}`
547
+ : ""}
548
+ </option>
549
+ ))}
550
+ </select>
551
+ </ToolbarTooltip>
552
+
553
+ <ToolbarTooltipButton
554
+ type="button"
555
+ variant="outline"
556
+ size="icon"
557
+ className="h-7 w-7 shrink-0"
558
+ content="走完全流程:从所选生命周期节点一次性执行完整任务链"
559
+ aria-label="走完全流程"
560
+ disabled={!debug.selectedLifecycleNodeId || debug.running}
561
+ onClick={() => void debug.onRunAll()}
562
+ >
563
+ <IconRunAll className="h-3.5 w-3.5" />
564
+ </ToolbarTooltipButton>
565
+
566
+ <ToolbarTooltipButton
567
+ type="button"
568
+ variant="outline"
569
+ size="icon"
570
+ className="h-7 w-7 shrink-0"
571
+ content="回到开始:清空已执行记录,从所选生命周期节点重新调试"
572
+ aria-label="回到开始"
573
+ disabled={
574
+ !debug.selectedLifecycleNodeId ||
575
+ debug.running ||
576
+ !debug.canResetToStart
577
+ }
578
+ onClick={() => void debug.onResetToStart()}
579
+ >
580
+ <IconResetToStart className="h-3.5 w-3.5" />
581
+ </ToolbarTooltipButton>
582
+
583
+ <ToolbarTooltipButton
584
+ type="button"
585
+ variant="outline"
586
+ size="icon"
587
+ className="h-7 w-7 shrink-0"
588
+ content="上一步:撤销最近一次节点执行,恢复日志与画布状态"
589
+ aria-label="上一步"
590
+ disabled={
591
+ !debug.selectedLifecycleNodeId ||
592
+ debug.running ||
593
+ !debug.canStepBack
594
+ }
595
+ onClick={() => void debug.onStepBack()}
596
+ >
597
+ <IconStepBack className="h-3.5 w-3.5" />
598
+ </ToolbarTooltipButton>
599
+
600
+ <ToolbarTooltipButton
601
+ type="button"
602
+ variant="outline"
603
+ size="icon"
604
+ className="h-7 w-7 shrink-0"
605
+ content={
606
+ debug.falseSignalHalt
607
+ ? "当前节点输出假信号,无法继续下一步"
608
+ : debug.chainComplete
609
+ ? "已经是最后一个节点,任务链已走完"
610
+ : "下一步:向下一节点发送真信号,每次执行一个节点"
611
+ }
612
+ aria-label="下一步"
613
+ disabled={
614
+ !debug.selectedLifecycleNodeId ||
615
+ debug.running ||
616
+ !debug.canStepNext
617
+ }
618
+ onClick={() => void debug.onStepNext()}
619
+ >
620
+ <IconStepNext className="h-3.5 w-3.5" />
621
+ </ToolbarTooltipButton>
622
+
623
+ <ToolbarTooltip content="任务输出日志:在右侧查看节点执行时间、输入与输出">
624
+ <Button
625
+ type="button"
626
+ variant={debug.logPanelOpen ? "default" : "outline"}
627
+ size="icon"
628
+ className="h-7 w-7 shrink-0"
629
+ aria-label="任务输出日志"
630
+ aria-pressed={debug.logPanelOpen}
631
+ onClick={debug.onToggleLogPanel}
632
+ >
633
+ <IconLog className="h-3.5 w-3.5" />
634
+ </Button>
635
+ </ToolbarTooltip>
636
+ </>
637
+ ) : null}
638
+ </div>
639
+ </TooltipProvider>
640
+
641
+ <BlueprintRenameDialog
642
+ open={renameTarget !== null}
643
+ initialName={renameTarget?.name ?? ""}
644
+ onOpenChange={(open) => {
645
+ if (!open) setRenameTarget(null);
646
+ }}
647
+ onConfirm={handleRenameConfirm}
648
+ />
649
+
650
+ <BlueprintDeleteDialog
651
+ open={deleteTarget !== null}
652
+ blueprintName={deleteTarget?.name ?? ""}
653
+ onOpenChange={(open) => {
654
+ if (!open) setDeleteTarget(null);
655
+ }}
656
+ onConfirm={() => {
657
+ if (!deleteTarget) return;
658
+ onDeleteItem(deleteTarget.id);
659
+ setDeleteTarget(null);
660
+ }}
661
+ />
662
+ </>
663
+ );
664
+ }