@arronqzy/react-blueprint 1.0.6 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/react-blueprint",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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.5",
16
- "@arronqzy/i18n": "0.1.3",
15
+ "@arronqzy/blueprint-dsl": "1.0.7",
17
16
  "@arronqzy/react-rx-store": "1.1.4",
17
+ "@arronqzy/i18n": "0.1.6",
18
18
  "@arronqzy/ui": "1.0.5"
19
19
  },
20
20
  "devDependencies": {
@@ -12,7 +12,7 @@ export type BlueprintContextMenuState =
12
12
  clientX: number;
13
13
  clientY: number;
14
14
  nodeId: string;
15
- role: "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "clock";
15
+ role: "blueprint" | "logic" | "and" | "lifecycle" | "event" | "fetch" | "json" | "clock";
16
16
  }
17
17
  | {
18
18
  kind: "edge";
@@ -6,11 +6,15 @@ import {
6
6
  SelectItem,
7
7
  SelectTrigger,
8
8
  SelectValue,
9
+ Checkbox,
9
10
  } from "@arronqzy/ui";
10
11
  import {
11
12
  LIFECYCLE_NODE_TYPE,
12
13
  PAGE_LIFECYCLE_PHASES,
14
+ VIEW_EVENT_TYPES,
15
+ type ExecutionTraceEntry,
13
16
  type PageLifecyclePhase,
17
+ type ViewEventType,
14
18
  } from "@arronqzy/blueprint-dsl";
15
19
  import { useI18n } from "@arronqzy/i18n/react";
16
20
 
@@ -19,13 +23,18 @@ import { ClockNodeConfigPanel } from "./components/ClockNodeConfigPanel";
19
23
  import { JsonNodeConfigPanel } from "./components/JsonNodeConfigPanel";
20
24
  import { LogicNodeConfigPanel } from "./components/LogicNodeConfigPanel";
21
25
  import { ViewElementMultiSelect } from "./components/ViewElementMultiSelect";
26
+ import { ConfigFieldLabel, ConfigHintIcon, ConfigSectionTitle } from "./components/ConfigHintIcon";
22
27
  import {
23
28
  getLifecyclePhaseLabel,
29
+ getViewEventTypeLabel,
30
+ inspectEventNodeInputs,
24
31
  patchNodeConfigSource,
25
32
  pruneViewElementIds,
26
33
  resolveBlueprintConfigSource,
34
+ resolveNodeEventConfig,
27
35
  resolveViewElementIds,
28
36
  type BlueprintConfigSource,
37
+ type BlueprintGraphEdge,
29
38
  type BlueprintGraphNode,
30
39
  type BlueprintNodeRole,
31
40
  } from "./graph/document";
@@ -42,6 +51,9 @@ export type BlueprintLibraryOption = {
42
51
 
43
52
  export type BlueprintNodeConfigSidebarProps = {
44
53
  node: BlueprintGraphNode;
54
+ graphNodes?: BlueprintGraphNode[];
55
+ graphEdges?: BlueprintGraphEdge[];
56
+ traceEntries?: ExecutionTraceEntry[];
45
57
  viewElementOptions?: BlueprintViewElementOption[];
46
58
  blueprintLibraryOptions?: BlueprintLibraryOption[];
47
59
  allowFalseSignalPropagation?: boolean;
@@ -64,6 +76,7 @@ export type BlueprintNodeConfigSidebarProps = {
64
76
  | "jsonConfig"
65
77
  | "logicConfig"
66
78
  | "clockConfig"
79
+ | "eventConfig"
67
80
  >
68
81
  >
69
82
  ) => void;
@@ -77,10 +90,14 @@ const ROLE_LABEL_KEYS: Record<BlueprintNodeRole, string> = {
77
90
  json: "blueprint.config.roleJson",
78
91
  logic: "blueprint.config.roleLogic",
79
92
  clock: "blueprint.config.roleClock",
93
+ event: "blueprint.config.roleEvent",
80
94
  };
81
95
 
82
96
  export function BlueprintNodeConfigSidebar({
83
97
  node,
98
+ graphNodes = [],
99
+ graphEdges = [],
100
+ traceEntries = [],
84
101
  viewElementOptions = [],
85
102
  blueprintLibraryOptions = [],
86
103
  allowFalseSignalPropagation = false,
@@ -90,6 +107,7 @@ export function BlueprintNodeConfigSidebar({
90
107
  const { t } = useI18n();
91
108
  const configSource = resolveBlueprintConfigSource(node);
92
109
  const linkedViewElementIds = resolveViewElementIds(node);
110
+ const selectedEventTypes = resolveNodeEventConfig(node).eventTypes;
93
111
  const existingViewElementIdSet = useMemo(
94
112
  () => new Set(viewElementOptions.map((opt) => opt.id)),
95
113
  [viewElementOptions]
@@ -99,7 +117,7 @@ export function BlueprintNodeConfigSidebar({
99
117
  );
100
118
 
101
119
  useEffect(() => {
102
- if (configSource !== "view") return;
120
+ if (configSource !== "view" && configSource !== "event") return;
103
121
  // 视图画布尚未就绪时不清理,避免误删有效绑定导致绑定节点「执行了但不写 scope」
104
122
  if (viewElementOptions.length === 0) return;
105
123
  const linked = resolveViewElementIds(node);
@@ -109,7 +127,7 @@ export function BlueprintNodeConfigSidebar({
109
127
  onUpdateNode(node.id, {
110
128
  viewElementIds: pruned.length > 0 ? pruned : undefined,
111
129
  viewElementId: undefined,
112
- configSource: "view",
130
+ configSource,
113
131
  });
114
132
  }, [
115
133
  configSource,
@@ -122,6 +140,10 @@ export function BlueprintNodeConfigSidebar({
122
140
  ]);
123
141
 
124
142
  const roleLabel = t(ROLE_LABEL_KEYS[node.role] ?? "blueprint.config.roleLogic");
143
+ const eventInputs = useMemo(
144
+ () => inspectEventNodeInputs(node.id, graphNodes, graphEdges),
145
+ [graphEdges, graphNodes, node.id]
146
+ );
125
147
 
126
148
  return (
127
149
  <div className="flex h-full min-h-0 flex-col overflow-hidden bg-background text-foreground">
@@ -151,7 +173,7 @@ export function BlueprintNodeConfigSidebar({
151
173
  const nextSource = value as BlueprintConfigSource;
152
174
  onUpdateNode(node.id, {
153
175
  ...patchNodeConfigSource(node, nextSource),
154
- ...(nextSource === "view"
176
+ ...(nextSource === "view" || nextSource === "event"
155
177
  ? {
156
178
  viewElementIds: linkedViewElementIds,
157
179
  viewElementId: undefined,
@@ -174,14 +196,22 @@ export function BlueprintNodeConfigSidebar({
174
196
  <SelectItem value="fetch">{t("blueprint.config.configFetch")}</SelectItem>
175
197
  <SelectItem value="json">{t("blueprint.config.configJson")}</SelectItem>
176
198
  <SelectItem value="clock">{t("blueprint.config.configClock")}</SelectItem>
199
+ <SelectItem value="event">{t("blueprint.config.configEvent")}</SelectItem>
177
200
  <SelectItem value="view">{t("blueprint.config.configView")}</SelectItem>
178
201
  </SelectContent>
179
202
  </Select>
180
203
  </label>
181
204
 
182
- {configSource === "view" ? (
205
+ {configSource === "view" || configSource === "event" ? (
183
206
  <div className="block space-y-1">
184
- <span className="text-muted-foreground">{t("blueprint.config.linkedViewNodes")}</span>
207
+ <ConfigFieldLabel
208
+ label={t("blueprint.config.linkedViewNodes")}
209
+ hint={
210
+ configSource === "event"
211
+ ? t("blueprint.config.eventViewHint")
212
+ : t("blueprint.config.viewMultiHint")
213
+ }
214
+ />
185
215
  <ViewElementMultiSelect
186
216
  options={viewElementOptions}
187
217
  value={linkedViewElementIds}
@@ -190,15 +220,11 @@ export function BlueprintNodeConfigSidebar({
190
220
  onUpdateNode(node.id, {
191
221
  viewElementIds: next,
192
222
  viewElementId: undefined,
193
- configSource: "view",
223
+ configSource,
194
224
  })
195
225
  }
196
226
  />
197
- {linkedViewElementIds.length === 0 ? (
198
- <p className="text-[11px] text-muted-foreground">
199
- {t("blueprint.config.viewMultiHint")}
200
- </p>
201
- ) : (
227
+ {linkedViewElementIds.length > 0 ? (
202
228
  <p className="text-[11px] text-muted-foreground">
203
229
  {t("blueprint.config.linkedViewCount", {
204
230
  count: linkedViewElementIds.length,
@@ -207,16 +233,16 @@ export function BlueprintNodeConfigSidebar({
207
233
  .join("、"),
208
234
  })}
209
235
  </p>
210
- )}
236
+ ) : null}
211
237
  </div>
212
238
  ) : null}
213
239
 
214
240
  {configSource === "blueprint" ? (
215
241
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
216
- <div className="font-medium text-foreground">{t("blueprint.config.blueprintAttrs")}</div>
217
- <p className="text-[11px] text-muted-foreground">
218
- {t("blueprint.config.blueprintAttrsHint")}
219
- </p>
242
+ <ConfigSectionTitle
243
+ title={t("blueprint.config.blueprintAttrs")}
244
+ hint={t("blueprint.config.blueprintAttrsHint")}
245
+ />
220
246
  <label className="block space-y-1">
221
247
  <span className="text-muted-foreground">{t("blueprint.config.refLibrary")}</span>
222
248
  <Select
@@ -262,15 +288,70 @@ export function BlueprintNodeConfigSidebar({
262
288
  </div>
263
289
  ) : null}
264
290
 
291
+ {configSource === "event" ? (
292
+ <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
293
+ <div className="flex items-center gap-1.5">
294
+ <div className="font-medium text-foreground">
295
+ {t("blueprint.config.eventListen")}
296
+ </div>
297
+ <ConfigHintIcon label={t("blueprint.config.eventListen")}>
298
+ <p>{t("blueprint.config.eventNoInput")}</p>
299
+ <p>{t("blueprint.config.eventHint")}</p>
300
+ </ConfigHintIcon>
301
+ </div>
302
+ {eventInputs.nonLifecycleCount > 0 ? (
303
+ <p className="rounded border border-destructive/40 bg-destructive/10 px-2 py-1.5 text-[11px] leading-relaxed text-destructive">
304
+ {t("blueprint.config.eventInputNotLifecycle")}
305
+ </p>
306
+ ) : eventInputs.lifecycleCount === 0 ? (
307
+ <p className="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] leading-relaxed text-amber-700 dark:text-amber-300">
308
+ {t("blueprint.config.eventInputMissing")}
309
+ </p>
310
+ ) : null}
311
+ <div className="space-y-1.5">
312
+ {VIEW_EVENT_TYPES.map((eventType) => {
313
+ const checked = selectedEventTypes.includes(eventType);
314
+ return (
315
+ <label key={eventType} className="flex items-center gap-2">
316
+ <Checkbox
317
+ checked={checked}
318
+ className="h-3.5 w-3.5 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
319
+ onCheckedChange={(nextChecked: boolean | "indeterminate") => {
320
+ const nextTypes: ViewEventType[] =
321
+ nextChecked === true
322
+ ? [...new Set([...selectedEventTypes, eventType])]
323
+ : selectedEventTypes.filter((type) => type !== eventType);
324
+ onUpdateNode(node.id, {
325
+ eventConfig: {
326
+ eventTypes: nextTypes.length > 0 ? nextTypes : ["click"],
327
+ },
328
+ configSource: "event",
329
+ });
330
+ }}
331
+ />
332
+ <span>{getViewEventTypeLabel(t, eventType)}</span>
333
+ </label>
334
+ );
335
+ })}
336
+ </div>
337
+ </div>
338
+ ) : null}
339
+
265
340
  {configSource === "lifecycle" ? (
266
341
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
267
- <div className="font-medium text-foreground">{t("blueprint.config.lifecycleHook")}</div>
268
- <p className="text-[11px] text-muted-foreground">
269
- {t("blueprint.config.lifecycleNoInput")}
270
- {node.lifecyclePhase === "blueprintActivated"
271
- ? t("blueprint.config.lifecycleBlueprintActivatedHint")
272
- : t("blueprint.config.lifecyclePageHint")}
273
- </p>
342
+ <div className="flex items-center gap-1.5">
343
+ <div className="font-medium text-foreground">
344
+ {t("blueprint.config.lifecycleHook")}
345
+ </div>
346
+ <ConfigHintIcon label={t("blueprint.config.lifecycleHook")}>
347
+ <p>{t("blueprint.config.lifecycleNoInput")}</p>
348
+ <p>
349
+ {node.lifecyclePhase === "blueprintActivated"
350
+ ? t("blueprint.config.lifecycleBlueprintActivatedHint")
351
+ : t("blueprint.config.lifecyclePageHint")}
352
+ </p>
353
+ </ConfigHintIcon>
354
+ </div>
274
355
  <label className="block space-y-1">
275
356
  <span className="text-muted-foreground">{t("blueprint.config.listenPhase")}</span>
276
357
  <Select
@@ -305,7 +386,13 @@ export function BlueprintNodeConfigSidebar({
305
386
  ) : null}
306
387
 
307
388
  {configSource === "fetch" ? (
308
- <FetchNodeConfigPanel node={node} onUpdateNode={onUpdateNode} />
389
+ <FetchNodeConfigPanel
390
+ node={node}
391
+ graphNodes={graphNodes}
392
+ graphEdges={graphEdges}
393
+ traceEntries={traceEntries}
394
+ onUpdateNode={onUpdateNode}
395
+ />
309
396
  ) : null}
310
397
 
311
398
  {configSource === "json" ? (
@@ -318,10 +405,10 @@ export function BlueprintNodeConfigSidebar({
318
405
 
319
406
  {configSource === "and" ? (
320
407
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
321
- <div className="font-medium text-foreground">{t("blueprint.config.andTitle")}</div>
322
- <p className="text-[11px] text-muted-foreground">
323
- {t("blueprint.config.andHint")}
324
- </p>
408
+ <ConfigSectionTitle
409
+ title={t("blueprint.config.andTitle")}
410
+ hint={t("blueprint.config.andHint")}
411
+ />
325
412
  </div>
326
413
  ) : null}
327
414
 
@@ -6,6 +6,7 @@ import { ClockFlowNode } from "./nodes/ClockFlowNode";
6
6
  import { FetchFlowNode } from "./nodes/FetchFlowNode";
7
7
  import { JsonFlowNode } from "./nodes/JsonFlowNode";
8
8
  import { LifecycleFlowNode } from "./nodes/LifecycleFlowNode";
9
+ import { EventFlowNode } from "./nodes/EventFlowNode";
9
10
  import { LogicFlowNode } from "./nodes/LogicFlowNode";
10
11
 
11
12
  /** 稳定引用,避免 React Flow 因 nodeTypes 变化反复卸载节点 */
@@ -17,4 +18,5 @@ export const blueprintNodeTypes = {
17
18
  json: JsonFlowNode,
18
19
  logic: LogicFlowNode,
19
20
  lifecycle: LifecycleFlowNode,
21
+ event: EventFlowNode,
20
22
  } satisfies NodeTypes;
@@ -5,6 +5,7 @@ import { useI18n } from "@arronqzy/i18n/react";
5
5
 
6
6
  import type { BlueprintGraphNode } from "../graph/document";
7
7
  import { resolveNodeClockConfig } from "../graph/document";
8
+ import { ConfigFieldLabel, ConfigSectionTitle } from "./ConfigHintIcon";
8
9
 
9
10
  export type ClockNodeConfigPanelProps = {
10
11
  node: BlueprintGraphNode;
@@ -73,17 +74,16 @@ export function ClockNodeConfigPanel({
73
74
 
74
75
  return (
75
76
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
76
- <div className="font-medium text-foreground">
77
- {t("blueprint.config.clockTitle")}
78
- </div>
79
- <p className="text-[11px] text-muted-foreground">
80
- {t("blueprint.config.clockHint")}
81
- </p>
77
+ <ConfigSectionTitle
78
+ title={t("blueprint.config.clockTitle")}
79
+ hint={t("blueprint.config.clockHint")}
80
+ />
82
81
 
83
82
  <label className="block space-y-1">
84
- <span className="text-muted-foreground">
85
- {t("blueprint.config.clockIntervalSeconds")}
86
- </span>
83
+ <ConfigFieldLabel
84
+ label={t("blueprint.config.clockIntervalSeconds")}
85
+ hint={t("blueprint.config.clockIntervalHint")}
86
+ />
87
87
  <Input
88
88
  type="number"
89
89
  min={0}
@@ -92,15 +92,13 @@ export function ClockNodeConfigPanel({
92
92
  onChange={handleIntervalChange}
93
93
  className="h-8"
94
94
  />
95
- <p className="text-[11px] text-muted-foreground">
96
- {t("blueprint.config.clockIntervalHint")}
97
- </p>
98
95
  </label>
99
96
 
100
97
  <label className="block space-y-1">
101
- <span className="text-muted-foreground">
102
- {t("blueprint.config.outputCount")}
103
- </span>
98
+ <ConfigFieldLabel
99
+ label={t("blueprint.config.outputCount")}
100
+ hint={t("blueprint.config.outputCountHint")}
101
+ />
104
102
  <Input
105
103
  type="number"
106
104
  min={1}
@@ -109,9 +107,6 @@ export function ClockNodeConfigPanel({
109
107
  onChange={handleOutputCountChange}
110
108
  className="h-8"
111
109
  />
112
- <p className="text-[11px] text-muted-foreground">
113
- {t("blueprint.config.outputCountHint")}
114
- </p>
115
110
  </label>
116
111
 
117
112
  <label className="flex items-start gap-2">
@@ -127,9 +122,10 @@ export function ClockNodeConfigPanel({
127
122
  </label>
128
123
 
129
124
  <label className="block space-y-1">
130
- <span className="text-muted-foreground">
131
- {t("blueprint.config.timeFormat")}
132
- </span>
125
+ <ConfigFieldLabel
126
+ label={t("blueprint.config.timeFormat")}
127
+ hint={t("blueprint.config.timeFormatHint")}
128
+ />
133
129
  <Input
134
130
  value={clockConfig.timeFormat}
135
131
  onChange={handleFormatChange}
@@ -137,9 +133,6 @@ export function ClockNodeConfigPanel({
137
133
  className="h-8 font-mono text-[11px]"
138
134
  placeholder="YYYY-MM-DD HH:mm:ss"
139
135
  />
140
- <p className="text-[11px] text-muted-foreground">
141
- {t("blueprint.config.timeFormatHint")}
142
- </p>
143
136
  </label>
144
137
  </div>
145
138
  );
@@ -0,0 +1,83 @@
1
+ import type { ReactNode } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
3
+ import {
4
+ Tooltip,
5
+ TooltipContent,
6
+ TooltipProvider,
7
+ TooltipTrigger,
8
+ } from "@arronqzy/ui";
9
+
10
+ export type ConfigHintIconProps = {
11
+ label?: string;
12
+ children: ReactNode;
13
+ className?: string;
14
+ contentClassName?: string;
15
+ };
16
+
17
+ export function ConfigHintIcon({
18
+ label,
19
+ children,
20
+ className,
21
+ contentClassName,
22
+ }: ConfigHintIconProps) {
23
+ const { t } = useI18n();
24
+ const resolvedLabel = label ?? t("common.hint");
25
+
26
+ return (
27
+ <TooltipProvider delayDuration={120}>
28
+ <Tooltip>
29
+ <TooltipTrigger asChild>
30
+ <button
31
+ type="button"
32
+ className={
33
+ className ??
34
+ "inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] leading-none text-muted-foreground hover:bg-accent/50"
35
+ }
36
+ aria-label={t("common.hintAria", { label: resolvedLabel })}
37
+ >
38
+ ?
39
+ </button>
40
+ </TooltipTrigger>
41
+ <TooltipContent
42
+ side="top"
43
+ className={`z-[10120] max-w-[360px] space-y-1.5 text-[11px] leading-5 ${contentClassName ?? ""}`}
44
+ >
45
+ {children}
46
+ </TooltipContent>
47
+ </Tooltip>
48
+ </TooltipProvider>
49
+ );
50
+ }
51
+
52
+ export function ConfigSectionTitle({
53
+ title,
54
+ hint,
55
+ }: {
56
+ title: string;
57
+ hint: ReactNode;
58
+ }) {
59
+ return (
60
+ <div className="flex items-center gap-1.5">
61
+ <div className="font-medium text-foreground">{title}</div>
62
+ <ConfigHintIcon label={title}>{hint}</ConfigHintIcon>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export function ConfigFieldLabel({
68
+ label,
69
+ hint,
70
+ }: {
71
+ label: string;
72
+ hint?: ReactNode;
73
+ }) {
74
+ if (!hint) {
75
+ return <span className="text-muted-foreground">{label}</span>;
76
+ }
77
+ return (
78
+ <span className="inline-flex items-center gap-1 text-muted-foreground">
79
+ {label}
80
+ <ConfigHintIcon label={label}>{hint}</ConfigHintIcon>
81
+ </span>
82
+ );
83
+ }