@nbt-dev/components 0.0.5

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 (66) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +10 -0
  3. package/TRADEMARKS.md +49 -0
  4. package/dist/chunk-3ZM6YOA4.js +704 -0
  5. package/dist/chunk-3ZM6YOA4.js.map +7 -0
  6. package/dist/chunk-7B2T5ZNG.js +467 -0
  7. package/dist/chunk-7B2T5ZNG.js.map +7 -0
  8. package/dist/chunk-S7VBQE6Y.js +636 -0
  9. package/dist/chunk-S7VBQE6Y.js.map +7 -0
  10. package/dist/chunk-UPEOXMLZ.js +625 -0
  11. package/dist/chunk-UPEOXMLZ.js.map +7 -0
  12. package/dist/core/auth.d.ts +13 -0
  13. package/dist/core/bulk-decoder.d.ts +13 -0
  14. package/dist/core/config.d.ts +10 -0
  15. package/dist/core/data-store.d.ts +20 -0
  16. package/dist/core/index.d.ts +9 -0
  17. package/dist/core/use-bulk-stream.d.ts +24 -0
  18. package/dist/core/use-cartridge-info.d.ts +14 -0
  19. package/dist/core/utils.d.ts +2 -0
  20. package/dist/editor/index.d.ts +7 -0
  21. package/dist/editor/index.js +16 -0
  22. package/dist/editor/index.js.map +7 -0
  23. package/dist/editor/lsp-client.d.ts +57 -0
  24. package/dist/editor/lsp-extensions.d.ts +4 -0
  25. package/dist/editor/nbt-editor.d.ts +13 -0
  26. package/dist/editor/nbt-language.d.ts +7 -0
  27. package/dist/generated/bulk-protocol.d.ts +36 -0
  28. package/dist/graph/diagram.d.ts +5 -0
  29. package/dist/graph/entity-graph-utils.d.ts +92 -0
  30. package/dist/graph/entity-node.d.ts +9 -0
  31. package/dist/graph/index.d.ts +5 -0
  32. package/dist/graph/index.js +19 -0
  33. package/dist/graph/index.js.map +7 -0
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.js +134 -0
  36. package/dist/index.js.map +7 -0
  37. package/dist/styles.css +2 -0
  38. package/dist/table/data-table.d.ts +9 -0
  39. package/dist/table/index.d.ts +3 -0
  40. package/dist/table/index.js +11 -0
  41. package/dist/table/index.js.map +7 -0
  42. package/dist/table/value-popover.d.ts +18 -0
  43. package/package.json +77 -0
  44. package/src/core/auth.ts +100 -0
  45. package/src/core/bulk-decoder.ts +178 -0
  46. package/src/core/config.tsx +39 -0
  47. package/src/core/data-store.ts +113 -0
  48. package/src/core/index.ts +34 -0
  49. package/src/core/use-bulk-stream.ts +412 -0
  50. package/src/core/use-cartridge-info.ts +100 -0
  51. package/src/core/utils.ts +6 -0
  52. package/src/editor/index.ts +13 -0
  53. package/src/editor/lsp-client.ts +227 -0
  54. package/src/editor/lsp-extensions.ts +191 -0
  55. package/src/editor/nbt-editor.tsx +142 -0
  56. package/src/editor/nbt-language.ts +151 -0
  57. package/src/generated/bulk-protocol.ts +63 -0
  58. package/src/graph/diagram.tsx +296 -0
  59. package/src/graph/entity-graph-utils.ts +423 -0
  60. package/src/graph/entity-node.tsx +122 -0
  61. package/src/graph/index.ts +19 -0
  62. package/src/index.ts +7 -0
  63. package/src/styles.css +94 -0
  64. package/src/table/data-table.tsx +274 -0
  65. package/src/table/index.ts +5 -0
  66. package/src/table/value-popover.tsx +230 -0
@@ -0,0 +1,151 @@
1
+ // NBT syntax highlighting for CodeMirror 6 — a StreamLanguage tokenizer ported
2
+ // from the console tokenizer (modules/lang_common/scan.jai) and the VSCode
3
+ // grammar (editors/vscode/nbt/syntaxes/nbt.tmLanguage.json). Highlighting is
4
+ // local; everything semantic (diagnostics/completion/hover) comes via the LSP.
5
+
6
+ import {
7
+ StreamLanguage,
8
+ LanguageSupport,
9
+ indentUnit,
10
+ type StreamParser,
11
+ } from "@codemirror/language";
12
+
13
+ const KEYWORDS = new Set([
14
+ "entity", "enum", "struct", "const", "export", "extends",
15
+ "fn", "on", "action", "activity", "task", "variant", "workflow", "command",
16
+ "middleware", "schedule", "every", "jai",
17
+ "migration", "test", "mock", "assert",
18
+ "cartridge", "service", "component", "app", "route", "layout",
19
+ "import", "from",
20
+ "if", "elif", "else", "while", "for", "in", "return", "break", "continue",
21
+ "delete", "defer", "print", "sleep", "fail",
22
+ ]);
23
+
24
+ const TYPES = new Set([
25
+ "string", "bool", "ulid", "document", "dict", "blob", "DateTime",
26
+ "u8", "u16", "u32", "u64", "s8", "s16", "s32", "s64",
27
+ "int", "integer", "float", "float64", "f32", "f64", "double",
28
+ "time", "bytes", "any", "name", "ref",
29
+ ]);
30
+
31
+ type NbtState = {
32
+ tripleString: boolean;
33
+ };
34
+
35
+ // NBT strings carry NO escapes (scan.jai consumes to the bare closing quote),
36
+ // so a backslash before a quote does not extend the string.
37
+ function eatSingleLineString(stream: StringStream, quote: string): string {
38
+ while (!stream.eol()) {
39
+ if (stream.next() === quote) break;
40
+ }
41
+ return "string";
42
+ }
43
+
44
+ type StringStream = Parameters<StreamParser<NbtState>["token"]>[0];
45
+
46
+ const nbtParser: StreamParser<NbtState> = {
47
+ name: "nbt",
48
+ startState: () => ({ tripleString: false }),
49
+ token(stream, state) {
50
+ if (state.tripleString) {
51
+ while (!stream.eol()) {
52
+ if (stream.match('"""')) {
53
+ state.tripleString = false;
54
+ return "string";
55
+ }
56
+ stream.next();
57
+ }
58
+ return "string";
59
+ }
60
+
61
+ if (stream.eatSpace()) return null;
62
+
63
+ const ch = stream.peek();
64
+ if (ch == null) return null;
65
+
66
+ if (ch === "#") {
67
+ stream.skipToEnd();
68
+ return "comment";
69
+ }
70
+
71
+ if (stream.match('"""')) {
72
+ state.tripleString = true;
73
+ // Rest of line is string body unless it closes on the same line.
74
+ while (!stream.eol()) {
75
+ if (stream.match('"""')) {
76
+ state.tripleString = false;
77
+ return "string";
78
+ }
79
+ stream.next();
80
+ }
81
+ return "string";
82
+ }
83
+
84
+ if (ch === '"') {
85
+ stream.next();
86
+ return eatSingleLineString(stream, '"');
87
+ }
88
+ if (ch === "'") {
89
+ // Like the scanner: only a string opener when not glued to an identifier
90
+ // (lets contractions inside docs pass).
91
+ const before = stream.string.charAt(stream.pos - 1);
92
+ if (!/[A-Za-z0-9_]/.test(before)) {
93
+ stream.next();
94
+ return eatSingleLineString(stream, "'");
95
+ }
96
+ stream.next();
97
+ return null;
98
+ }
99
+
100
+ // f-strings: f"..." — body highlighted as a string (interpolation holes
101
+ // are cosmetic only).
102
+ if (ch === "f" && stream.string.charAt(stream.pos + 1) === '"') {
103
+ stream.next();
104
+ stream.next();
105
+ return eatSingleLineString(stream, '"');
106
+ }
107
+
108
+ // @attr / @@attr
109
+ if (ch === "@") {
110
+ stream.next();
111
+ stream.eat("@");
112
+ stream.eatWhile(/[A-Za-z0-9_]/);
113
+ return "meta";
114
+ }
115
+
116
+ // Numbers. The optional fraction backtracks on `0..9` ranges (matches "0",
117
+ // not "0.").
118
+ if (/\d/.test(ch)) {
119
+ stream.match(/^\d+(\.\d+)?/);
120
+ return "number";
121
+ }
122
+
123
+ if (/[A-Za-z_]/.test(ch)) {
124
+ stream.eatWhile(/[A-Za-z0-9_]/);
125
+ const word = stream.current();
126
+ if (word === "true" || word === "false") return "atom";
127
+ if (KEYWORDS.has(word)) return "keyword";
128
+ if (TYPES.has(word)) return "typeName";
129
+ if (/^[A-Z]/.test(word)) return "typeName"; // entity/enum references
130
+ return "variableName";
131
+ }
132
+
133
+ if (/[+\-*/=<>!&|?.,:;]/.test(ch)) {
134
+ stream.next();
135
+ stream.eatWhile(/[+\-*/=<>!&|.]/);
136
+ return "operator";
137
+ }
138
+
139
+ stream.next();
140
+ return null;
141
+ },
142
+ languageData: {
143
+ commentTokens: { line: "#" },
144
+ },
145
+ };
146
+
147
+ export const nbtLanguage = StreamLanguage.define(nbtParser);
148
+
149
+ export function nbtLanguageSupport(): LanguageSupport {
150
+ return new LanguageSupport(nbtLanguage, [indentUnit.of(" ")]);
151
+ }
@@ -0,0 +1,63 @@
1
+ // Wire constants mirrored from modules/cart/ws.jai (CART_WS_* + cart_ws_type_byte).
2
+
3
+ // Frame types (first byte of each WS binary message).
4
+ export const FRAME_SCHEMA = 0x01;
5
+ export const FRAME_DATA = 0x02;
6
+ export const FRAME_DATA_END = 0x03;
7
+ export const FRAME_DELTA_INS = 0x04;
8
+ export const FRAME_DELTA_UPD = 0x05;
9
+ export const FRAME_DELTA_DEL = 0x06;
10
+ export const FRAME_ERROR = 0xff;
11
+
12
+ // Column type bytes (schema frame, per column).
13
+ export const TYPE_U8 = 0x01;
14
+ export const TYPE_U16 = 0x02;
15
+ export const TYPE_U32 = 0x03;
16
+ export const TYPE_U64 = 0x04;
17
+ export const TYPE_S8 = 0x05;
18
+ export const TYPE_S16 = 0x06;
19
+ export const TYPE_S32 = 0x07;
20
+ export const TYPE_S64 = 0x08;
21
+ export const TYPE_BOOL = 0x09;
22
+ export const TYPE_FLOAT32 = 0x0a;
23
+ export const TYPE_FLOAT64 = 0x0b;
24
+ export const TYPE_STRING = 0x0c;
25
+ export const TYPE_DATETIME = 0x0d;
26
+ export const TYPE_DOCUMENT = 0x0e;
27
+
28
+ export type ColumnDef = {
29
+ name: string;
30
+ type: number;
31
+ fixedSize: number; // 0 for variable-width (string, document)
32
+ };
33
+
34
+ export type SchemaFrame = {
35
+ totalRows: number;
36
+ columns: ColumnDef[];
37
+ };
38
+
39
+ export type DeltaFrame = {
40
+ op: number;
41
+ rowData?: string[];
42
+ id?: string; // DELETE carries the row id as a len-prefixed string
43
+ };
44
+
45
+ export function fixedSizeForType(type: number): number {
46
+ switch (type) {
47
+ case TYPE_U8: return 1;
48
+ case TYPE_U16: return 2;
49
+ case TYPE_U32: return 4;
50
+ case TYPE_U64: return 8;
51
+ case TYPE_S8: return 1;
52
+ case TYPE_S16: return 2;
53
+ case TYPE_S32: return 4;
54
+ case TYPE_S64: return 8;
55
+ case TYPE_BOOL: return 1;
56
+ case TYPE_FLOAT32: return 4;
57
+ case TYPE_FLOAT64: return 8;
58
+ case TYPE_STRING: return 0;
59
+ case TYPE_DATETIME: return 8;
60
+ case TYPE_DOCUMENT: return 0;
61
+ default: return 0;
62
+ }
63
+ }
@@ -0,0 +1,296 @@
1
+ import React from "react";
2
+ import {
3
+ Background,
4
+ BackgroundVariant,
5
+ Controls,
6
+ MarkerType,
7
+ ReactFlow,
8
+ ReactFlowProvider,
9
+ useNodesInitialized,
10
+ useReactFlow,
11
+ type Edge,
12
+ type Node,
13
+ } from "@xyflow/react";
14
+ // ReactFlow's stylesheet ships inlined in @nbt-dev/devtools/styles.css.
15
+ import { useDevToolsConfig } from "../core/config";
16
+ import { authHeaders } from "../core/auth";
17
+ import { useBulkRowCounts } from "../core/use-bulk-stream";
18
+ import type { BulkRegistry } from "../core/use-cartridge-info";
19
+ import { EntityNode } from "./entity-node";
20
+ import {
21
+ buildEntityGraphModel,
22
+ cartsFromContracts,
23
+ entityGraphId,
24
+ filterEntityGraphModel,
25
+ type Contract,
26
+ type EntityGraphModel,
27
+ type EntityGraphNodeData,
28
+ } from "./entity-graph-utils";
29
+
30
+ const nodeTypes = { entity: EntityNode };
31
+
32
+ // Custom nodes are measured asynchronously after mount, so the `fitView` prop
33
+ // fits to zero-size bounds on first paint (boxes end up off-screen). Refit once
34
+ // nodes report measured dimensions, and again whenever the visible set changes.
35
+ const FitOnReady: React.FC<{ fitKey: string }> = ({ fitKey }) => {
36
+ const initialized = useNodesInitialized();
37
+ const { fitView } = useReactFlow();
38
+ React.useEffect(() => {
39
+ if (initialized) fitView({ padding: 0.25, duration: 200 });
40
+ }, [initialized, fitKey, fitView]);
41
+ return null;
42
+ };
43
+
44
+ type EntityFlowEdge = Edge<Record<string, unknown>, "smoothstep"> & {
45
+ pathOptions: { borderRadius: number; offset: number };
46
+ };
47
+
48
+ function useContracts() {
49
+ const { apiBaseUrl } = useDevToolsConfig();
50
+ const [contracts, setContracts] = React.useState<Contract[]>([]);
51
+ const [loading, setLoading] = React.useState(true);
52
+ const [error, setError] = React.useState<string | null>(null);
53
+
54
+ React.useEffect(() => {
55
+ const ac = new AbortController();
56
+ let cancelled = false;
57
+ (async () => {
58
+ try {
59
+ const r = await fetch(`${apiBaseUrl}/_console/contracts`, {
60
+ signal: ac.signal,
61
+ credentials: "include",
62
+ headers: authHeaders(),
63
+ });
64
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
65
+ const data = (await r.json()) as Contract[];
66
+ if (!Array.isArray(data)) throw new Error("malformed contracts response");
67
+ if (!cancelled) setContracts(data);
68
+ } catch (e) {
69
+ if (cancelled || (e as { name?: string }).name === "AbortError") return;
70
+ setError(e instanceof Error ? e.message : String(e));
71
+ } finally {
72
+ if (!cancelled) setLoading(false);
73
+ }
74
+ })();
75
+ return () => {
76
+ cancelled = true;
77
+ ac.abort();
78
+ };
79
+ }, [apiBaseUrl]);
80
+
81
+ return { contracts, loading, error };
82
+ }
83
+
84
+ // Derive the bulk WS registry off the same contracts so live row counts work
85
+ // through the shared socket (the provider preloads each entity's SCHEMA, which
86
+ // carries totalRows). Mirrors buildRegistry in use-cartridge-info.
87
+ function registryFromContracts(contracts: Contract[]): BulkRegistry {
88
+ const reg: BulkRegistry = {};
89
+ for (const c of contracts) {
90
+ if (!c.cartridge || !c.owns) continue;
91
+ const entities = Object.keys(c.owns).map((name) => ({
92
+ name,
93
+ route: `/_ws/bulk/${c.cartridge}/${name.toLowerCase()}`,
94
+ searchFields: [] as string[],
95
+ }));
96
+ if (entities.length > 0) reg[c.cartridge] = entities;
97
+ }
98
+ return reg;
99
+ }
100
+
101
+ // Renders the entity graph. Must be mounted inside a <BulkStreamProvider> — it
102
+ // reads live row counts off that shared socket's SCHEMA preload rather than
103
+ // opening its own connection (the Data tab already provides one).
104
+ export const DiagramView: React.FC<{
105
+ visibleIds: Set<string>;
106
+ onSelectNode?: (cart: string, entity: string) => void;
107
+ }> = ({ visibleIds, onSelectNode }) => {
108
+ const { contracts, loading, error } = useContracts();
109
+
110
+ const graph = React.useMemo(
111
+ () => buildEntityGraphModel(cartsFromContracts(contracts)),
112
+ [contracts],
113
+ );
114
+ const registry = React.useMemo(() => registryFromContracts(contracts), [contracts]);
115
+
116
+ if (graph.nodes.length === 0) {
117
+ let msg = "No installed cartridges with entities.";
118
+ if (loading) msg = "Loading entity graph…";
119
+ else if (error) msg = `Failed to load contracts: ${error}`;
120
+ return <div className="p-3 text-[12px] text-muted-foreground">{msg}</div>;
121
+ }
122
+
123
+ return (
124
+ <DiagramInner
125
+ graph={graph}
126
+ registry={registry}
127
+ visibleIds={visibleIds}
128
+ onSelectNode={onSelectNode}
129
+ />
130
+ );
131
+ };
132
+
133
+ const DiagramInner: React.FC<{
134
+ graph: EntityGraphModel;
135
+ registry: BulkRegistry;
136
+ visibleIds: Set<string>;
137
+ onSelectNode?: (cart: string, entity: string) => void;
138
+ }> = ({ graph, registry, visibleIds, onSelectNode }) => {
139
+ const rowCounts = useBulkRowCounts(registry);
140
+ const [focusedNodeId, setFocusedNodeId] = React.useState<string | null>(null);
141
+
142
+ const visibleGraph = React.useMemo(
143
+ () => filterEntityGraphModel(graph, visibleIds),
144
+ [graph, visibleIds],
145
+ );
146
+
147
+ React.useEffect(() => {
148
+ if (focusedNodeId && !visibleIds.has(focusedNodeId)) setFocusedNodeId(null);
149
+ }, [focusedNodeId, visibleIds]);
150
+
151
+ const focusedConnections = React.useMemo(() => {
152
+ if (!focusedNodeId) {
153
+ return {
154
+ connectedIds: new Set<string>(),
155
+ edgeIds: new Set<string>(),
156
+ fieldsByNode: new Map<string, Set<string>>(),
157
+ };
158
+ }
159
+ const connectedIds = new Set<string>([focusedNodeId]);
160
+ const edgeIds = new Set<string>();
161
+ const fieldsByNode = new Map<string, Set<string>>();
162
+ const addField = (nodeId: string, fieldName: string) => {
163
+ const fields = fieldsByNode.get(nodeId) ?? new Set<string>();
164
+ fields.add(fieldName);
165
+ fieldsByNode.set(nodeId, fields);
166
+ };
167
+ for (const edge of visibleGraph.edges) {
168
+ if (edge.source !== focusedNodeId && edge.target !== focusedNodeId) continue;
169
+ edgeIds.add(edge.id);
170
+ connectedIds.add(edge.source);
171
+ connectedIds.add(edge.target);
172
+ addField(edge.source, edge.sourceField);
173
+ addField(edge.target, edge.targetField);
174
+ }
175
+ return { connectedIds, edgeIds, fieldsByNode };
176
+ }, [focusedNodeId, visibleGraph.edges]);
177
+
178
+ const nodes: Node[] = React.useMemo(
179
+ () =>
180
+ visibleGraph.nodes.map((node) => ({
181
+ id: node.id,
182
+ type: "entity",
183
+ position: node.position,
184
+ data: {
185
+ ...node,
186
+ rowCount: rowCounts[node.id] ?? node.rowCount,
187
+ highlight: focusedNodeId
188
+ ? {
189
+ focused: node.id === focusedNodeId,
190
+ connected: focusedConnections.connectedIds.has(node.id) && node.id !== focusedNodeId,
191
+ dimmed: !focusedConnections.connectedIds.has(node.id),
192
+ fields: [...(focusedConnections.fieldsByNode.get(node.id) ?? [])],
193
+ }
194
+ : undefined,
195
+ },
196
+ })),
197
+ [focusedConnections, focusedNodeId, visibleGraph.nodes, rowCounts],
198
+ );
199
+
200
+ const edges: EntityFlowEdge[] = React.useMemo(() => {
201
+ const nodeById = new Map(visibleGraph.nodes.map((node) => [node.id, node]));
202
+ const laneCounts = new Map<string, number>();
203
+ return visibleGraph.edges.map((edge) => {
204
+ const source = nodeById.get(edge.source);
205
+ const target = nodeById.get(edge.target);
206
+ const sourceSide = source && target && source.position.x > target.position.x ? "left" : "right";
207
+ const targetSide = sourceSide === "left" ? "right" : "left";
208
+ const laneKey = `${edge.target}:${edge.targetField}:${targetSide}`;
209
+ const lane = laneCounts.get(laneKey) ?? 0;
210
+ laneCounts.set(laneKey, lane + 1);
211
+ const highlighted = !focusedNodeId || focusedConnections.edgeIds.has(edge.id);
212
+ return {
213
+ id: edge.id,
214
+ source: edge.source,
215
+ target: edge.target,
216
+ sourceHandle: `source-${edge.sourceField}-${sourceSide}`,
217
+ targetHandle: `target-${edge.targetField}-${targetSide}`,
218
+ type: "smoothstep",
219
+ pathOptions: { borderRadius: 8, offset: 22 + (lane % 5) * 12 },
220
+ markerEnd: {
221
+ type: MarkerType.ArrowClosed,
222
+ color: highlighted ? "#2563eb" : "#64748b",
223
+ },
224
+ style: {
225
+ strokeWidth: focusedNodeId && highlighted ? 2.5 : 1.5,
226
+ stroke: highlighted ? "#2563eb" : "#64748b",
227
+ opacity: highlighted ? 1 : 0.16,
228
+ },
229
+ };
230
+ });
231
+ }, [focusedConnections.edgeIds, focusedNodeId, visibleGraph.edges, visibleGraph.nodes]);
232
+
233
+ const onNodeClick = React.useCallback(
234
+ (_: unknown, node: Node) => {
235
+ const data = node.data as EntityGraphNodeData;
236
+ const nodeId = entityGraphId(data.cartridge, data.entity);
237
+ setFocusedNodeId((prev) => (prev === nodeId ? null : nodeId));
238
+ onSelectNode?.(data.cartridge, data.entity);
239
+ },
240
+ [onSelectNode],
241
+ );
242
+
243
+ // Right-click surfaces the same data panel (and suppresses the browser menu)
244
+ // rather than toggling focus off; selecting also focuses the node.
245
+ const onNodeContextMenu = React.useCallback(
246
+ (e: React.MouseEvent, node: Node) => {
247
+ e.preventDefault();
248
+ const data = node.data as EntityGraphNodeData;
249
+ setFocusedNodeId(entityGraphId(data.cartridge, data.entity));
250
+ onSelectNode?.(data.cartridge, data.entity);
251
+ },
252
+ [onSelectNode],
253
+ );
254
+
255
+ const clearFocusedNode = React.useCallback(() => setFocusedNodeId(null), []);
256
+
257
+ return (
258
+ <div className="flex h-full min-h-0 w-full flex-col">
259
+ <div className="relative min-h-0 flex-1 bg-zinc-950">
260
+ {nodes.length === 0 ? (
261
+ <div className="flex h-full items-center justify-center text-[12px] text-zinc-500">
262
+ No entities are visible.
263
+ </div>
264
+ ) : (
265
+ <ReactFlowProvider>
266
+ <ReactFlow
267
+ colorMode="dark"
268
+ nodes={nodes}
269
+ edges={edges}
270
+ nodeTypes={nodeTypes}
271
+ fitView
272
+ fitViewOptions={{ padding: 0.2 }}
273
+ minZoom={0.1}
274
+ maxZoom={1.6}
275
+ nodesDraggable={false}
276
+ nodesConnectable={false}
277
+ elementsSelectable
278
+ panOnDrag
279
+ zoomOnScroll
280
+ zoomOnPinch
281
+ selectionOnDrag={false}
282
+ onNodeClick={onNodeClick}
283
+ onNodeContextMenu={onNodeContextMenu}
284
+ onPaneClick={clearFocusedNode}
285
+ proOptions={{ hideAttribution: true }}
286
+ >
287
+ <FitOnReady fitKey={[...visibleIds].sort().join("|")} />
288
+ <Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#3f3f46" />
289
+ <Controls showInteractive={false} />
290
+ </ReactFlow>
291
+ </ReactFlowProvider>
292
+ )}
293
+ </div>
294
+ </div>
295
+ );
296
+ };