@arronqzy/react-blueprint 1.0.19 → 1.0.21

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.
@@ -0,0 +1,98 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
2
+ import type { ApiCollectionRecord } from "@arronqzy/blueprint-dsl";
3
+
4
+ const DB_NAME = "arronqzy-api-collections";
5
+ const DB_VERSION = 1;
6
+ const STORE_NAME = "collections";
7
+
8
+ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
9
+ return new Promise((resolve, reject) => {
10
+ const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
11
+ request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
12
+ request.onupgradeneeded = () => {
13
+ const db = request.result;
14
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
15
+ const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
16
+ store.createIndex("updatedAt", "updatedAt", { unique: false });
17
+ }
18
+ };
19
+ request.onsuccess = () => resolve(request.result);
20
+ });
21
+ }
22
+
23
+ function runTransaction<T>(
24
+ mode: IDBTransactionMode,
25
+ runner: (store: IDBObjectStore) => IDBRequest<T>,
26
+ nameSpace?: string | null
27
+ ): Promise<T> {
28
+ return openDb(nameSpace).then(
29
+ (db) =>
30
+ new Promise<T>((resolve, reject) => {
31
+ const tx = db.transaction(STORE_NAME, mode);
32
+ const store = tx.objectStore(STORE_NAME);
33
+ const request = runner(store);
34
+ let result!: T;
35
+
36
+ request.onerror = () =>
37
+ reject(request.error ?? new Error("indexeddb-request-failed"));
38
+ request.onsuccess = () => {
39
+ result = request.result as T;
40
+ };
41
+
42
+ tx.oncomplete = () => {
43
+ db.close();
44
+ resolve(result);
45
+ };
46
+ tx.onerror = () => {
47
+ db.close();
48
+ reject(tx.error ?? new Error("indexeddb-transaction-failed"));
49
+ };
50
+ })
51
+ );
52
+ }
53
+
54
+ export async function listApiCollectionRecords(
55
+ nameSpace?: string | null
56
+ ): Promise<ApiCollectionRecord[]> {
57
+ const records = await runTransaction<ApiCollectionRecord[]>(
58
+ "readonly",
59
+ (store) => store.getAll(),
60
+ nameSpace
61
+ );
62
+ return records.sort((a, b) => b.updatedAt - a.updatedAt);
63
+ }
64
+
65
+ export async function getApiCollectionRecord(
66
+ id: string,
67
+ nameSpace?: string | null
68
+ ): Promise<ApiCollectionRecord | null> {
69
+ const record = await runTransaction<ApiCollectionRecord | undefined>(
70
+ "readonly",
71
+ (store) => store.get(id),
72
+ nameSpace
73
+ );
74
+ return record ?? null;
75
+ }
76
+
77
+ export async function putApiCollectionRecord(
78
+ record: ApiCollectionRecord,
79
+ nameSpace?: string | null
80
+ ): Promise<ApiCollectionRecord> {
81
+ await runTransaction<IDBValidKey>(
82
+ "readwrite",
83
+ (store) => store.put(record),
84
+ nameSpace
85
+ );
86
+ return record;
87
+ }
88
+
89
+ export async function deleteApiCollectionRecord(
90
+ id: string,
91
+ nameSpace?: string | null
92
+ ): Promise<void> {
93
+ await runTransaction<undefined>(
94
+ "readwrite",
95
+ (store) => store.delete(id),
96
+ nameSpace
97
+ );
98
+ }
@@ -0,0 +1,139 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ apiCollectionToListItem,
4
+ createApiCollectionId,
5
+ parseApiCollectionJson,
6
+ type ApiCollectionDocument,
7
+ type ApiCollectionListItem,
8
+ type ApiCollectionRecord,
9
+ } from "@arronqzy/blueprint-dsl";
10
+
11
+ import {
12
+ deleteApiCollectionRecord,
13
+ listApiCollectionRecords,
14
+ putApiCollectionRecord,
15
+ } from "./api-collection-db";
16
+
17
+ type Listener = () => void;
18
+
19
+ let nameSpaceRef: string | null | undefined = null;
20
+ let records: ApiCollectionRecord[] = [];
21
+ let hydrated = false;
22
+ const listeners = new Set<Listener>();
23
+
24
+ function emit() {
25
+ for (const listener of listeners) listener();
26
+ }
27
+
28
+ function sortRecords(list: ApiCollectionRecord[]): ApiCollectionRecord[] {
29
+ return [...list].sort((a, b) => b.updatedAt - a.updatedAt);
30
+ }
31
+
32
+ export function getApiCollectionRecords(): ApiCollectionRecord[] {
33
+ return records;
34
+ }
35
+
36
+ export function getApiCollectionListItems(): ApiCollectionListItem[] {
37
+ return records.map(apiCollectionToListItem);
38
+ }
39
+
40
+ export function getApiCollectionById(id: string): ApiCollectionRecord | null {
41
+ return records.find((item) => item.id === id) ?? null;
42
+ }
43
+
44
+ export function subscribeApiCollections(listener: Listener): () => void {
45
+ listeners.add(listener);
46
+ return () => listeners.delete(listener);
47
+ }
48
+
49
+ export async function hydrateApiCollections(
50
+ nameSpace?: string | null
51
+ ): Promise<ApiCollectionRecord[]> {
52
+ nameSpaceRef = nameSpace;
53
+ const persisted = await listApiCollectionRecords(nameSpace);
54
+ const memoryOnly = records.filter((item) => !item.persistLocal);
55
+ const persistedIds = new Set(persisted.map((item) => item.id));
56
+ records = sortRecords([
57
+ ...persisted,
58
+ ...memoryOnly.filter((item) => !persistedIds.has(item.id)),
59
+ ]);
60
+ hydrated = true;
61
+ emit();
62
+ return records;
63
+ }
64
+
65
+ export type UpsertApiCollectionInput = {
66
+ id?: string;
67
+ rawJson: string;
68
+ persistLocal: boolean;
69
+ /** 未写 document.name 时用文件名 */
70
+ fallbackName?: string;
71
+ };
72
+
73
+ export type UpsertApiCollectionResult =
74
+ | { ok: true; record: ApiCollectionRecord }
75
+ | { ok: false; error: string };
76
+
77
+ export async function upsertApiCollectionFromJson(
78
+ input: UpsertApiCollectionInput
79
+ ): Promise<UpsertApiCollectionResult> {
80
+ const parsed = parseApiCollectionJson(input.rawJson);
81
+ if (!parsed.ok) return parsed;
82
+
83
+ const document: ApiCollectionDocument = {
84
+ ...parsed.document,
85
+ name:
86
+ parsed.document.name.trim() ||
87
+ input.fallbackName?.replace(/\.json$/i, "").trim() ||
88
+ parsed.document.name,
89
+ };
90
+ const now = Date.now();
91
+ const existing = input.id
92
+ ? records.find((item) => item.id === input.id) ?? null
93
+ : null;
94
+ const record: ApiCollectionRecord = {
95
+ id: existing?.id ?? createApiCollectionId(),
96
+ name: document.name,
97
+ persistLocal: input.persistLocal,
98
+ document,
99
+ rawJson: JSON.stringify(document, null, 2),
100
+ createdAt: existing?.createdAt ?? now,
101
+ updatedAt: now,
102
+ };
103
+
104
+ if (record.persistLocal) {
105
+ await putApiCollectionRecord(record, nameSpaceRef);
106
+ } else if (existing?.persistLocal) {
107
+ await deleteApiCollectionRecord(record.id, nameSpaceRef);
108
+ }
109
+
110
+ const next = records.filter((item) => item.id !== record.id);
111
+ next.push(record);
112
+ records = sortRecords(next);
113
+ emit();
114
+ return { ok: true, record };
115
+ }
116
+
117
+ export async function removeApiCollection(id: string): Promise<void> {
118
+ const existing = records.find((item) => item.id === id);
119
+ if (existing?.persistLocal || hydrated) {
120
+ try {
121
+ await deleteApiCollectionRecord(id, nameSpaceRef);
122
+ } catch {
123
+ // 内存-only 记录在库里可能不存在
124
+ }
125
+ }
126
+ records = records.filter((item) => item.id !== id);
127
+ emit();
128
+ }
129
+
130
+ export function useApiCollections(): ApiCollectionRecord[] {
131
+ const [list, setList] = useState<ApiCollectionRecord[]>(() => records);
132
+ useEffect(() => subscribeApiCollections(() => setList([...records])), []);
133
+ return list;
134
+ }
135
+
136
+ export function useApiCollectionListItems(): ApiCollectionListItem[] {
137
+ const list = useApiCollections();
138
+ return list.map(apiCollectionToListItem);
139
+ }
@@ -20,6 +20,7 @@ export function AndFlowNode({ id, data }: NodeProps) {
20
20
  <div
21
21
  className={cn(
22
22
  "bp-node bp-node--and",
23
+ nodeData.isSelected && "bp-node--selected",
23
24
  executionTone === "success" && "bp-node--execution-true",
24
25
  executionTone === "error" && "bp-node--execution-false"
25
26
  )}
@@ -29,7 +30,7 @@ export function AndFlowNode({ id, data }: NodeProps) {
29
30
  position={Position.Left}
30
31
  id="inA"
31
32
  className="bp-flow-handle bp-flow-handle--target"
32
- style={{ top: "35%" }}
33
+ style={{ top: "32%" }}
33
34
  title={t("blueprint.node.andInA")}
34
35
  />
35
36
  <Handle
@@ -37,7 +38,7 @@ export function AndFlowNode({ id, data }: NodeProps) {
37
38
  position={Position.Left}
38
39
  id="inB"
39
40
  className="bp-flow-handle bp-flow-handle--target"
40
- style={{ top: "65%" }}
41
+ style={{ top: "68%" }}
41
42
  title={t("blueprint.node.andInB")}
42
43
  />
43
44
  <BlueprintNodeCard
@@ -1,7 +1,10 @@
1
1
  import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
2
2
  import type { NodeProps } from "@xyflow/react";
3
3
 
4
- import { resolveBlueprintNodeTypeLabel } from "../graph/document";
4
+ import {
5
+ resolveBlueprintConfigSource,
6
+ resolveBlueprintNodeTypeLabel,
7
+ } from "../graph/document";
5
8
  import { resolveBlueprintNodeSummary } from "../graph/node-summary";
6
9
  import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
7
10
  import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
@@ -12,6 +15,8 @@ export function BlueprintFlowNode({ id, data }: NodeProps) {
12
15
  const { t } = useI18n();
13
16
  const nodeData = data as BlueprintFlowNodeData;
14
17
  const onSelect = useBlueprintNodeSelect();
18
+ const configSource = resolveBlueprintConfigSource(nodeData);
19
+ const variant = configSource === "view" ? "view" : "blueprint";
15
20
 
16
21
  return (
17
22
  <BlueprintNodeShell
@@ -19,7 +24,7 @@ export function BlueprintFlowNode({ id, data }: NodeProps) {
19
24
  label={nodeData.label}
20
25
  meta={resolveBlueprintNodeTypeLabel(nodeData, t)}
21
26
  subtitle={resolveBlueprintNodeSummary(nodeData, t)}
22
- variant="blueprint"
27
+ variant={variant}
23
28
  selected={Boolean(nodeData.isSelected)}
24
29
  executionTone={resolveBlueprintNodeExecutionTone(nodeData)}
25
30
  onSelect={onSelect}
@@ -1,5 +1,6 @@
1
1
  import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
2
2
  import { cn } from "@arronqzy/ui";
3
+ import type { ReactNode } from "react";
3
4
 
4
5
  export type BlueprintNodeCardProps = {
5
6
  nodeId: string;
@@ -9,78 +10,130 @@ export type BlueprintNodeCardProps = {
9
10
  subtitle?: string;
10
11
  /** 调试执行进度(如时钟 2/3) */
11
12
  progressLabel?: string;
12
- variant?: "blueprint" | "logic" | "and" | "lifecycle" | "event" | "fetch" | "json" | "storage" | "clock";
13
+ variant?: "blueprint" | "view" | "logic" | "and" | "lifecycle" | "event" | "fetch" | "json" | "storage" | "clock";
13
14
  selected?: boolean;
14
15
  hideLeadingDot?: boolean;
15
16
  onSelect?: (nodeId: string) => void;
16
17
  };
17
18
 
18
- const variantStyle = {
19
- blueprint: {
20
- accent: "border-l-primary",
21
- badge: "bg-primary/10 text-primary",
22
- dot: "bg-primary",
23
- },
24
- logic: {
25
- accent: "border-l-sky-500 dark:border-l-sky-400",
26
- badge: "bg-sky-500/10 text-sky-700 dark:text-sky-300",
27
- dot: "bg-sky-500",
28
- },
29
- and: {
30
- accent: "border-l-indigo-500 dark:border-l-indigo-400",
31
- badge: "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300",
32
- dot: "bg-indigo-500",
33
- },
34
- lifecycle: {
35
- accent: "border-l-amber-500 dark:border-l-amber-400",
36
- badge: "bg-amber-500/10 text-amber-700 dark:text-amber-300",
37
- dot: "bg-amber-500",
38
- },
39
- event: {
40
- accent: "border-l-fuchsia-500 dark:border-l-fuchsia-400",
41
- badge: "bg-fuchsia-500/10 text-fuchsia-700 dark:text-fuchsia-300",
42
- dot: "bg-fuchsia-500",
43
- },
44
- fetch: {
45
- accent: "border-l-violet-500 dark:border-l-violet-400",
46
- badge: "bg-violet-500/10 text-violet-700 dark:text-violet-300",
47
- dot: "bg-violet-500",
48
- },
49
- json: {
50
- accent: "border-l-teal-500 dark:border-l-teal-400",
51
- badge: "bg-teal-500/10 text-teal-700 dark:text-teal-300",
52
- dot: "bg-teal-500",
53
- },
54
- storage: {
55
- accent: "border-l-emerald-500 dark:border-l-emerald-400",
56
- badge: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
57
- dot: "bg-emerald-500",
58
- },
59
- clock: {
60
- accent: "border-l-rose-500 dark:border-l-rose-400",
61
- badge: "bg-rose-500/10 text-rose-700 dark:text-rose-300",
62
- dot: "bg-rose-500",
63
- },
64
- } as const;
65
-
66
- function NodeGrip() {
19
+ function IconSvg({ children }: { children: ReactNode }) {
67
20
  return (
68
- <div
69
- className="flex shrink-0 flex-col gap-[3px] opacity-35"
21
+ <svg
22
+ viewBox="0 0 24 24"
23
+ fill="none"
24
+ stroke="currentColor"
25
+ strokeWidth="2"
26
+ strokeLinecap="round"
27
+ strokeLinejoin="round"
70
28
  aria-hidden="true"
71
29
  >
72
- <span className="flex gap-[3px]">
73
- <span className="h-[3px] w-[3px] rounded-full bg-current" />
74
- <span className="h-[3px] w-[3px] rounded-full bg-current" />
75
- </span>
76
- <span className="flex gap-[3px]">
77
- <span className="h-[3px] w-[3px] rounded-full bg-current" />
78
- <span className="h-[3px] w-[3px] rounded-full bg-current" />
79
- </span>
80
- </div>
30
+ {children}
31
+ </svg>
81
32
  );
82
33
  }
83
34
 
35
+ function VariantIcon({
36
+ variant,
37
+ }: {
38
+ variant: NonNullable<BlueprintNodeCardProps["variant"]>;
39
+ }) {
40
+ switch (variant) {
41
+ case "logic":
42
+ return (
43
+ <IconSvg>
44
+ <path d="M6 4v6a4 4 0 0 0 4 4h4" />
45
+ <path d="M14 14v6" />
46
+ <circle cx="6" cy="4" r="1.5" fill="currentColor" stroke="none" />
47
+ <circle cx="14" cy="20" r="1.5" fill="currentColor" stroke="none" />
48
+ <circle cx="18" cy="14" r="1.5" fill="currentColor" stroke="none" />
49
+ </IconSvg>
50
+ );
51
+ case "and":
52
+ return (
53
+ <IconSvg>
54
+ <path d="M8 6v12" />
55
+ <path d="M8 12h8" />
56
+ <path d="M16 6v12" />
57
+ </IconSvg>
58
+ );
59
+ case "lifecycle":
60
+ return (
61
+ <IconSvg>
62
+ <circle cx="12" cy="12" r="7" />
63
+ <path d="M12 8v4l2.5 1.5" />
64
+ </IconSvg>
65
+ );
66
+ case "event":
67
+ return (
68
+ <IconSvg>
69
+ <path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z" />
70
+ </IconSvg>
71
+ );
72
+ case "fetch":
73
+ return (
74
+ <IconSvg>
75
+ <path d="M12 3v12" />
76
+ <path d="m7 10 5 5 5-5" />
77
+ <path d="M5 19h14" />
78
+ </IconSvg>
79
+ );
80
+ case "json":
81
+ return (
82
+ <IconSvg>
83
+ <path d="M8 4c-2 0-3 1.5-3 4s1 4 3 4" />
84
+ <path d="M8 12c-2 0-3 1.5-3 4s1 4 3 4" />
85
+ <path d="M16 4c2 0 3 1.5 3 4s-1 4-3 4" />
86
+ <path d="M16 12c2 0 3 1.5 3 4s-1 4-3 4" />
87
+ </IconSvg>
88
+ );
89
+ case "storage":
90
+ return (
91
+ <IconSvg>
92
+ <ellipse cx="12" cy="6" rx="7" ry="3" />
93
+ <path d="M5 6v6c0 1.7 3.1 3 7 3s7-1.3 7-3V6" />
94
+ <path d="M5 12v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6" />
95
+ </IconSvg>
96
+ );
97
+ case "clock":
98
+ return (
99
+ <IconSvg>
100
+ <circle cx="12" cy="12" r="8" />
101
+ <path d="M12 8v4.5L15 15" />
102
+ </IconSvg>
103
+ );
104
+ case "blueprint":
105
+ return (
106
+ <IconSvg>
107
+ <circle cx="7" cy="7" r="2.5" />
108
+ <circle cx="17" cy="7" r="2.5" />
109
+ <circle cx="12" cy="17" r="2.5" />
110
+ <path d="M9.2 8.2 14.8 8.2" />
111
+ <path d="M8.2 9.2 11 14.8" />
112
+ <path d="M15.8 9.2 13 14.8" />
113
+ </IconSvg>
114
+ );
115
+ case "view":
116
+ return (
117
+ <IconSvg>
118
+ <rect x="3" y="5" width="18" height="14" rx="2" />
119
+ <path d="M3 9h18" />
120
+ <path d="M8 14h4" />
121
+ </IconSvg>
122
+ );
123
+ default:
124
+ return (
125
+ <IconSvg>
126
+ <circle cx="7" cy="7" r="2.5" />
127
+ <circle cx="17" cy="7" r="2.5" />
128
+ <circle cx="12" cy="17" r="2.5" />
129
+ <path d="M9.2 8.2 14.8 8.2" />
130
+ <path d="M8.2 9.2 11 14.8" />
131
+ <path d="M15.8 9.2 13 14.8" />
132
+ </IconSvg>
133
+ );
134
+ }
135
+ }
136
+
84
137
  export function BlueprintNodeCard({
85
138
  nodeId,
86
139
  label,
@@ -89,81 +142,42 @@ export function BlueprintNodeCard({
89
142
  progressLabel,
90
143
  variant = "blueprint",
91
144
  selected = false,
92
- hideLeadingDot = false,
93
145
  onSelect,
94
146
  }: BlueprintNodeCardProps) {
95
147
  const { t } = useI18n();
96
- const v = variantStyle[variant];
148
+ const showMeta = Boolean(meta && meta !== label);
97
149
 
98
150
  return (
99
151
  <div
100
152
  data-blueprint-node-card
101
153
  className={cn(
102
- "bp-node-card w-[168px] overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-sm",
103
- "transition-[box-shadow,border-color] duration-150",
104
- hideLeadingDot
105
- ? cn(
106
- "border-t-[3px]",
107
- variant === "event"
108
- ? "border-t-fuchsia-500 dark:border-t-fuchsia-400"
109
- : "border-t-amber-500 dark:border-t-amber-400"
110
- )
111
- : cn("border-l-[3px]", v.accent),
112
- selected &&
113
- "border-primary/50 shadow-[0_0_0_1px_hsl(var(--primary)/0.35),0_4px_12px_hsl(var(--primary)/0.12)]"
154
+ "bp-node-card bp-flow-drag-handle",
155
+ selected && "bp-node-card--selected"
114
156
  )}
157
+ title={t("blueprint.node.dragToMove")}
115
158
  >
116
- <div
117
- className={cn(
118
- "bp-flow-drag-handle flex cursor-grab items-center gap-2 border-b border-border/50 px-2 py-1.5",
119
- "bg-muted/25 text-muted-foreground active:cursor-grabbing"
120
- )}
121
- title={t("blueprint.node.dragToMove")}
122
- >
123
- <NodeGrip />
124
- {meta ? (
125
- <span
126
- className={cn(
127
- "min-w-0 flex-1 truncate rounded px-1.5 py-0.5 text-[10px] font-medium leading-none tracking-wide",
128
- v.badge
129
- )}
130
- title={meta}
131
- >
132
- {meta}
133
- </span>
134
- ) : null}
135
- </div>
136
-
137
159
  <button
138
160
  type="button"
139
- className={cn(
140
- "nodrag flex w-full text-left outline-none transition-colors hover:bg-accent/30",
141
- "focus-visible:ring-1 focus-visible:ring-primary focus-visible:ring-inset",
142
- hideLeadingDot
143
- ? cn("px-2.5 py-2", subtitle ? "items-start" : "items-center")
144
- : "items-start gap-2 px-2.5 py-2"
145
- )}
161
+ className="bp-node-card__body"
146
162
  onClick={(e) => {
147
163
  e.stopPropagation();
148
164
  onSelect?.(nodeId);
149
165
  }}
150
166
  >
151
- {!hideLeadingDot ? (
152
- <span
153
- className={cn("mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full", v.dot)}
154
- aria-hidden="true"
155
- />
156
- ) : null}
157
- <div className="min-w-0 flex-1">
158
- <div className="flex items-start gap-1.5">
159
- <div className="min-w-0 flex-1 truncate text-[13px] font-medium leading-snug text-foreground">
160
- {label}
161
- </div>
167
+ <span
168
+ className={cn("bp-node-icon", `bp-node-icon--${variant}`)}
169
+ aria-hidden="true"
170
+ >
171
+ <VariantIcon variant={variant} />
172
+ </span>
173
+ <div className="bp-node-card__content">
174
+ <div className="bp-node-card__title-row">
175
+ <div className="bp-node-card__title">{label}</div>
162
176
  {progressLabel ? (
163
177
  <span
164
178
  className={cn(
165
- "shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold leading-none",
166
- v.badge
179
+ "bp-node-card__badge",
180
+ `bp-node-card__badge--${variant}`
167
181
  )}
168
182
  title={t("blueprint.node.signalSentCount")}
169
183
  >
@@ -171,11 +185,13 @@ export function BlueprintNodeCard({
171
185
  </span>
172
186
  ) : null}
173
187
  </div>
188
+ {showMeta ? (
189
+ <div className="bp-node-card__meta" title={meta}>
190
+ {meta}
191
+ </div>
192
+ ) : null}
174
193
  {subtitle ? (
175
- <div
176
- className="mt-0.5 truncate font-mono text-[10px] leading-tight text-muted-foreground"
177
- title={subtitle}
178
- >
194
+ <div className="bp-node-card__subtitle" title={subtitle}>
179
195
  {subtitle}
180
196
  </div>
181
197
  ) : null}
@@ -27,6 +27,7 @@ export function BlueprintNodeShell({
27
27
  <div
28
28
  className={cn(
29
29
  "bp-node",
30
+ selected && "bp-node--selected",
30
31
  executionTone === "success" && "bp-node--execution-true",
31
32
  executionTone === "error" && "bp-node--execution-false"
32
33
  )}
@@ -20,6 +20,7 @@ export function LifecycleFlowNode({ id, data }: NodeProps) {
20
20
  <div
21
21
  className={cn(
22
22
  "bp-node bp-node--lifecycle",
23
+ nodeData.isSelected && "bp-node--selected",
23
24
  executionTone === "success" && "bp-node--execution-true",
24
25
  executionTone === "error" && "bp-node--execution-false"
25
26
  )}