@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,625 @@
1
+ "use client";
2
+ import {
3
+ authHeaders,
4
+ useBulkRowCounts,
5
+ useDevToolsConfig
6
+ } from "./chunk-3ZM6YOA4.js";
7
+
8
+ // src/graph/diagram.tsx
9
+ import React from "react";
10
+ import {
11
+ Background,
12
+ BackgroundVariant,
13
+ Controls,
14
+ MarkerType,
15
+ ReactFlow,
16
+ ReactFlowProvider,
17
+ useNodesInitialized,
18
+ useReactFlow
19
+ } from "@xyflow/react";
20
+
21
+ // src/graph/entity-node.tsx
22
+ import { Handle, Position } from "@xyflow/react";
23
+ import { Database, FileText, KeyRound, Link2 } from "lucide-react";
24
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
25
+ function EntityNode({ data, selected }) {
26
+ const node = data;
27
+ const highlight = node.highlight;
28
+ const highlightedFields = new Set(highlight?.fields ?? []);
29
+ const hiddenHandleClass = "!h-1 !w-1 !border-0 !bg-transparent !opacity-0";
30
+ return /* @__PURE__ */ jsxs(
31
+ "div",
32
+ {
33
+ className: [
34
+ "relative w-[260px] overflow-visible rounded-xl border bg-zinc-900 font-mono text-[11px] text-zinc-100 shadow-lg shadow-black/40 transition-opacity",
35
+ highlight?.focused ? "border-blue-500 ring-2 ring-blue-500/30" : highlight?.connected ? "border-blue-400/60 ring-2 ring-blue-500/10" : "border-zinc-700",
36
+ highlight?.dimmed ? "opacity-35" : "",
37
+ selected && !highlight?.focused ? "ring-2 ring-zinc-100/10" : ""
38
+ ].join(" "),
39
+ children: [
40
+ /* @__PURE__ */ jsxs("div", { className: "rounded-t-xl border-b border-zinc-700 bg-zinc-800 px-3 py-2", children: [
41
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
42
+ /* @__PURE__ */ jsx(Database, { className: "h-3.5 w-3.5 shrink-0 text-zinc-400" }),
43
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[13px] font-semibold leading-none text-zinc-50", children: node.entity })
44
+ ] }),
45
+ /* @__PURE__ */ jsx("div", { className: "mt-1 truncate text-[10px] text-zinc-500", children: node.cartridge })
46
+ ] }),
47
+ /* @__PURE__ */ jsx("div", { className: "bg-zinc-900 py-1", children: node.fields.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-[10px] text-zinc-500", children: "No fields" }) : node.fields.map((field) => {
48
+ const type = `${field.type}${field.array ? "[]" : ""}${field.optional ? "?" : ""}`;
49
+ const isId = field.displayName.toLowerCase() === "id";
50
+ const isRelation = field.kind === "relation";
51
+ const isDocument = field.kind === "document";
52
+ const fieldHighlighted = highlightedFields.has(field.displayName);
53
+ return /* @__PURE__ */ jsxs(
54
+ "div",
55
+ {
56
+ className: [
57
+ "relative grid min-h-[26px] grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-3 py-1 text-zinc-300",
58
+ fieldHighlighted ? "bg-blue-500/15 text-blue-200" : ""
59
+ ].join(" "),
60
+ children: [
61
+ isId ? /* @__PURE__ */ jsxs(Fragment, { children: [
62
+ /* @__PURE__ */ jsx(
63
+ Handle,
64
+ {
65
+ id: `target-${field.displayName}-left`,
66
+ type: "target",
67
+ position: Position.Left,
68
+ className: `!left-0 ${hiddenHandleClass}`,
69
+ style: { top: "50%" }
70
+ }
71
+ ),
72
+ /* @__PURE__ */ jsx(
73
+ Handle,
74
+ {
75
+ id: `target-${field.displayName}-right`,
76
+ type: "target",
77
+ position: Position.Right,
78
+ className: `!right-0 ${hiddenHandleClass}`,
79
+ style: { top: "50%" }
80
+ }
81
+ )
82
+ ] }) : null,
83
+ isRelation ? /* @__PURE__ */ jsxs(Fragment, { children: [
84
+ /* @__PURE__ */ jsx(
85
+ Handle,
86
+ {
87
+ id: `source-${field.displayName}-left`,
88
+ type: "source",
89
+ position: Position.Left,
90
+ className: `!left-0 ${hiddenHandleClass}`,
91
+ style: { top: "50%" }
92
+ }
93
+ ),
94
+ /* @__PURE__ */ jsx(
95
+ Handle,
96
+ {
97
+ id: `source-${field.displayName}-right`,
98
+ type: "source",
99
+ position: Position.Right,
100
+ className: `!right-0 ${hiddenHandleClass}`,
101
+ style: { top: "50%" }
102
+ }
103
+ )
104
+ ] }) : null,
105
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [
106
+ isId ? /* @__PURE__ */ jsx(KeyRound, { className: "h-3 w-3 shrink-0 text-zinc-500" }) : null,
107
+ isRelation ? /* @__PURE__ */ jsx(Link2, { className: ["h-3 w-3 shrink-0", fieldHighlighted ? "text-blue-300" : "text-blue-400"].join(" ") }) : null,
108
+ isDocument ? /* @__PURE__ */ jsx(FileText, { className: "h-3 w-3 shrink-0 text-zinc-500" }) : null,
109
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: field.displayName })
110
+ ] }),
111
+ /* @__PURE__ */ jsx("span", { className: "max-w-[96px] truncate text-right text-zinc-500", children: type })
112
+ ]
113
+ },
114
+ `${field.name}:${field.displayName}`
115
+ );
116
+ }) }),
117
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 rounded-b-xl border-t border-zinc-700 bg-zinc-800 px-3 py-1.5 text-[10px] text-zinc-500", children: [
118
+ /* @__PURE__ */ jsxs("span", { className: "tabular-nums", children: [
119
+ node.rowCount.toLocaleString(),
120
+ " rows"
121
+ ] }),
122
+ /* @__PURE__ */ jsxs("span", { className: "tabular-nums", children: [
123
+ node.relationCount,
124
+ " rel \xB7 ",
125
+ node.scalarCount,
126
+ " scalar"
127
+ ] })
128
+ ] })
129
+ ]
130
+ }
131
+ );
132
+ }
133
+
134
+ // src/graph/entity-graph-utils.ts
135
+ import dagre from "@dagrejs/dagre";
136
+ var NODE_WIDTH = 260;
137
+ var MIN_NODE_HEIGHT = 150;
138
+ var FIELD_ROW_HEIGHT = 26;
139
+ var COLUMN_WIDTH = 390;
140
+ function cartsFromContracts(contracts) {
141
+ const carts = [];
142
+ for (const c of contracts) {
143
+ if (!c.cartridge || !c.owns) continue;
144
+ const entities = [];
145
+ for (const [name, ent] of Object.entries(c.owns)) {
146
+ const fields = (ent.fields ?? []).map((f) => ({
147
+ name: f.name,
148
+ type: f.type,
149
+ optional: f.optional,
150
+ array: f.array,
151
+ kind: f.kind,
152
+ target: f.target,
153
+ targetCart: f.target_cart,
154
+ relationKind: f.relation_kind,
155
+ fkField: f.fk_field
156
+ }));
157
+ entities.push({ name, fields });
158
+ }
159
+ if (entities.length > 0) carts.push({ name: c.cartridge, entities });
160
+ }
161
+ return carts;
162
+ }
163
+ function entityGraphId(cartridge, entity) {
164
+ return `${cartridge}:${entity}`;
165
+ }
166
+ function fieldKind(field) {
167
+ return field.kind ?? (field.target ? "relation" : "scalar");
168
+ }
169
+ function graphField(field) {
170
+ const kind = fieldKind(field);
171
+ return {
172
+ name: field.name,
173
+ displayName: kind === "relation" ? field.fkField ?? field.name : field.name,
174
+ type: kind === "relation" && field.target ? field.target : field.type,
175
+ kind,
176
+ optional: field.optional === true,
177
+ array: field.array === true,
178
+ target: field.target,
179
+ targetCart: field.targetCart,
180
+ relationKind: field.relationKind,
181
+ fkField: field.fkField
182
+ };
183
+ }
184
+ function makeGraphFields(fields) {
185
+ const out = fields.map(graphField);
186
+ if (!out.some((field) => field.displayName.toLowerCase() === "id")) {
187
+ out.unshift({
188
+ name: "id",
189
+ displayName: "id",
190
+ type: "id",
191
+ kind: "scalar",
192
+ optional: false,
193
+ array: false,
194
+ implicit: true
195
+ });
196
+ }
197
+ return out;
198
+ }
199
+ function normalizeName(value) {
200
+ return value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
201
+ }
202
+ function words(value) {
203
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).map((part) => part.toLowerCase()).filter(Boolean);
204
+ }
205
+ function entityAliases(entity) {
206
+ const parts = words(entity);
207
+ const aliases = /* @__PURE__ */ new Set([normalizeName(entity)]);
208
+ const last = parts.length > 0 ? parts[parts.length - 1] : void 0;
209
+ if (last) aliases.add(last);
210
+ if (parts.length > 1 && last) {
211
+ aliases.add(`${parts.slice(0, -1).map((part) => part[0]).join("")}${last}`);
212
+ aliases.add(parts.map((part) => part[0]).join(""));
213
+ }
214
+ return [...aliases].filter((alias) => alias.length >= 2);
215
+ }
216
+ function inferTargetNode(fieldName, nodes) {
217
+ if (!/id$/i.test(fieldName) || /^id$/i.test(fieldName)) return null;
218
+ const base = normalizeName(fieldName.replace(/id$/i, ""));
219
+ if (base.length < 3) return null;
220
+ const candidates = nodes.flatMap(
221
+ (node) => entityAliases(node.entity).map((alias) => {
222
+ const exact = base === alias;
223
+ const suffix = !exact && alias.length >= 4 && base.endsWith(alias);
224
+ if (!exact && !suffix) return null;
225
+ return {
226
+ node,
227
+ score: (exact ? 1e3 : 500) + alias.length
228
+ };
229
+ })
230
+ ).filter((candidate) => candidate !== null).sort((a, b) => b.score - a.score || a.node.entity.localeCompare(b.node.entity));
231
+ const first = candidates[0];
232
+ if (!first) return null;
233
+ const second = candidates[1];
234
+ if (second && first.score === second.score) return null;
235
+ return first.node;
236
+ }
237
+ function markRelationField(node, fieldName, target) {
238
+ const field = node?.fields.find(
239
+ (candidate) => candidate.displayName === fieldName || candidate.name === fieldName
240
+ );
241
+ if (!field || field.displayName.toLowerCase() === "id") return;
242
+ field.kind = "relation";
243
+ field.target = target.entity;
244
+ field.targetCart = target.cartridge;
245
+ field.relationKind ?? (field.relationKind = "inferred");
246
+ field.fkField ?? (field.fkField = fieldName);
247
+ }
248
+ function recomputeNodeCounts(nodes) {
249
+ for (const node of nodes) {
250
+ node.scalarCount = node.fields.filter((field) => field.kind === "scalar").length;
251
+ node.relationCount = node.fields.filter((field) => field.kind === "relation").length;
252
+ node.documentCount = node.fields.filter((field) => field.kind === "document").length;
253
+ }
254
+ }
255
+ function nodeHeight(node) {
256
+ return Math.max(MIN_NODE_HEIGHT, 74 + node.fields.length * FIELD_ROW_HEIGHT);
257
+ }
258
+ function layoutNodes(nodes, edges) {
259
+ const degree = new Map(nodes.map((n) => [n.id, 0]));
260
+ for (const edge of edges) {
261
+ degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1);
262
+ degree.set(edge.target, (degree.get(edge.target) ?? 0) + 1);
263
+ }
264
+ const connected = nodes.filter((n) => (degree.get(n.id) ?? 0) > 0);
265
+ const isolated = nodes.filter((n) => (degree.get(n.id) ?? 0) === 0);
266
+ const connectedIds = new Set(connected.map((n) => n.id));
267
+ let maxX = 0;
268
+ let maxY = 0;
269
+ if (connected.length > 0) {
270
+ const g = new dagre.graphlib.Graph();
271
+ g.setGraph({ rankdir: "LR", ranksep: 160, nodesep: 48, marginx: 40, marginy: 40 });
272
+ g.setDefaultEdgeLabel(() => ({}));
273
+ for (const node of connected) {
274
+ g.setNode(node.id, { width: NODE_WIDTH, height: nodeHeight(node) });
275
+ }
276
+ for (const edge of edges) {
277
+ if (edge.source === edge.target) continue;
278
+ if (!connectedIds.has(edge.source) || !connectedIds.has(edge.target)) continue;
279
+ g.setEdge(edge.source, edge.target);
280
+ }
281
+ dagre.layout(g);
282
+ for (const node of connected) {
283
+ const p = g.node(node.id);
284
+ const h = nodeHeight(node);
285
+ node.position = { x: p.x - NODE_WIDTH / 2, y: p.y - h / 2 };
286
+ maxX = Math.max(maxX, node.position.x + NODE_WIDTH);
287
+ maxY = Math.max(maxY, node.position.y + h);
288
+ }
289
+ }
290
+ if (isolated.length > 0) {
291
+ isolated.sort((a, b) => a.entity.localeCompare(b.entity));
292
+ const startX = connected.length > 0 ? maxX + COLUMN_WIDTH : 0;
293
+ const perColumn = Math.max(1, Math.ceil(isolated.length / Math.ceil(isolated.length / 6)));
294
+ let x = startX;
295
+ let y = 0;
296
+ let inColumn = 0;
297
+ for (const node of isolated) {
298
+ node.position = { x, y };
299
+ y += nodeHeight(node) + 48;
300
+ inColumn += 1;
301
+ if (inColumn >= perColumn) {
302
+ inColumn = 0;
303
+ y = 0;
304
+ x += COLUMN_WIDTH;
305
+ }
306
+ }
307
+ }
308
+ }
309
+ function makeTotals(nodes, edges) {
310
+ return {
311
+ entities: nodes.length,
312
+ relationships: edges.length,
313
+ rows: nodes.reduce((sum, node) => sum + node.rowCount, 0)
314
+ };
315
+ }
316
+ function buildEntityGraphModel(cartridges) {
317
+ const sortedCarts = [...cartridges].sort((a, b) => a.name.localeCompare(b.name));
318
+ const nodes = [];
319
+ sortedCarts.forEach((cart, cartIndex) => {
320
+ const entities = [...cart.entities].sort((a, b) => a.name.localeCompare(b.name));
321
+ let y = 0;
322
+ entities.forEach((entity) => {
323
+ const id = entityGraphId(cart.name, entity.name);
324
+ const fields = entity.fields ?? [];
325
+ const graphFields = makeGraphFields(fields);
326
+ nodes.push({
327
+ id,
328
+ cartridge: cart.name,
329
+ entity: entity.name,
330
+ fields: graphFields,
331
+ fieldCount: graphFields.length,
332
+ scalarCount: graphFields.filter((field) => field.kind === "scalar").length,
333
+ relationCount: graphFields.filter((field) => field.kind === "relation").length,
334
+ documentCount: graphFields.filter((field) => field.kind === "document").length,
335
+ rowCount: 0,
336
+ position: { x: cartIndex * COLUMN_WIDTH, y }
337
+ });
338
+ y += Math.max(MIN_NODE_HEIGHT, 74 + graphFields.length * FIELD_ROW_HEIGHT) + 56;
339
+ });
340
+ });
341
+ const nodeIds = new Set(nodes.map((node) => node.id));
342
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
343
+ const edges = [];
344
+ const edgeKeys = /* @__PURE__ */ new Set();
345
+ for (const cart of sortedCarts) {
346
+ for (const entity of cart.entities) {
347
+ const source = entityGraphId(cart.name, entity.name);
348
+ const sourceNode = nodeById.get(source);
349
+ for (const field of entity.fields ?? []) {
350
+ const explicitRelation = fieldKind(field) === "relation" && !!field.target;
351
+ const inferredTargetNode = explicitRelation ? null : inferTargetNode(field.name, nodes);
352
+ if (!explicitRelation && !inferredTargetNode) continue;
353
+ const targetCart = explicitRelation ? field.targetCart ?? cart.name : inferredTargetNode?.cartridge;
354
+ const targetEntity = explicitRelation ? field.target : inferredTargetNode?.entity;
355
+ if (!targetCart || !targetEntity) continue;
356
+ const target = entityGraphId(targetCart, targetEntity);
357
+ if (!nodeIds.has(source) || !nodeIds.has(target)) continue;
358
+ const targetNode = nodeById.get(target);
359
+ const idField = targetNode?.fields.find((candidate) => candidate.displayName.toLowerCase() === "id");
360
+ const targetField = idField?.displayName ?? "__entity";
361
+ const sourceGraphField = sourceNode?.fields.find(
362
+ (candidate) => candidate.name === field.name || candidate.displayName === field.name
363
+ );
364
+ const sourceField = sourceGraphField?.displayName ?? field.fkField ?? field.name;
365
+ const edgeKey = `${source}:${sourceField}->${target}:${targetField}`;
366
+ if (edgeKeys.has(edgeKey)) continue;
367
+ edgeKeys.add(edgeKey);
368
+ if (!explicitRelation && targetNode) markRelationField(sourceNode, sourceField, targetNode);
369
+ edges.push({
370
+ id: `${source}:${field.name}->${target}`,
371
+ source,
372
+ target,
373
+ sourceField,
374
+ targetField,
375
+ label: field.name,
376
+ relationKind: field.relationKind ?? (!explicitRelation ? "inferred" : void 0),
377
+ fkField: field.fkField ?? (!explicitRelation ? field.name : void 0)
378
+ });
379
+ }
380
+ }
381
+ }
382
+ recomputeNodeCounts(nodes);
383
+ return {
384
+ nodes,
385
+ edges,
386
+ totals: makeTotals(nodes, edges)
387
+ };
388
+ }
389
+ function filterEntityGraphModel(model, visibleIds) {
390
+ const nodes = model.nodes.filter((node) => visibleIds.has(node.id)).map((node) => ({ ...node, position: { ...node.position } }));
391
+ const ids = new Set(nodes.map((node) => node.id));
392
+ const edges = model.edges.filter((edge) => ids.has(edge.source) && ids.has(edge.target));
393
+ layoutNodes(nodes, edges);
394
+ return {
395
+ nodes,
396
+ edges,
397
+ totals: makeTotals(nodes, edges)
398
+ };
399
+ }
400
+
401
+ // src/graph/diagram.tsx
402
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
403
+ var nodeTypes = { entity: EntityNode };
404
+ var FitOnReady = ({ fitKey }) => {
405
+ const initialized = useNodesInitialized();
406
+ const { fitView } = useReactFlow();
407
+ React.useEffect(() => {
408
+ if (initialized) fitView({ padding: 0.25, duration: 200 });
409
+ }, [initialized, fitKey, fitView]);
410
+ return null;
411
+ };
412
+ function useContracts() {
413
+ const { apiBaseUrl } = useDevToolsConfig();
414
+ const [contracts, setContracts] = React.useState([]);
415
+ const [loading, setLoading] = React.useState(true);
416
+ const [error, setError] = React.useState(null);
417
+ React.useEffect(() => {
418
+ const ac = new AbortController();
419
+ let cancelled = false;
420
+ (async () => {
421
+ try {
422
+ const r = await fetch(`${apiBaseUrl}/_console/contracts`, {
423
+ signal: ac.signal,
424
+ credentials: "include",
425
+ headers: authHeaders()
426
+ });
427
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
428
+ const data = await r.json();
429
+ if (!Array.isArray(data)) throw new Error("malformed contracts response");
430
+ if (!cancelled) setContracts(data);
431
+ } catch (e) {
432
+ if (cancelled || e.name === "AbortError") return;
433
+ setError(e instanceof Error ? e.message : String(e));
434
+ } finally {
435
+ if (!cancelled) setLoading(false);
436
+ }
437
+ })();
438
+ return () => {
439
+ cancelled = true;
440
+ ac.abort();
441
+ };
442
+ }, [apiBaseUrl]);
443
+ return { contracts, loading, error };
444
+ }
445
+ function registryFromContracts(contracts) {
446
+ const reg = {};
447
+ for (const c of contracts) {
448
+ if (!c.cartridge || !c.owns) continue;
449
+ const entities = Object.keys(c.owns).map((name) => ({
450
+ name,
451
+ route: `/_ws/bulk/${c.cartridge}/${name.toLowerCase()}`,
452
+ searchFields: []
453
+ }));
454
+ if (entities.length > 0) reg[c.cartridge] = entities;
455
+ }
456
+ return reg;
457
+ }
458
+ var DiagramView = ({ visibleIds, onSelectNode }) => {
459
+ const { contracts, loading, error } = useContracts();
460
+ const graph = React.useMemo(
461
+ () => buildEntityGraphModel(cartsFromContracts(contracts)),
462
+ [contracts]
463
+ );
464
+ const registry = React.useMemo(() => registryFromContracts(contracts), [contracts]);
465
+ if (graph.nodes.length === 0) {
466
+ let msg = "No installed cartridges with entities.";
467
+ if (loading) msg = "Loading entity graph\u2026";
468
+ else if (error) msg = `Failed to load contracts: ${error}`;
469
+ return /* @__PURE__ */ jsx2("div", { className: "p-3 text-[12px] text-muted-foreground", children: msg });
470
+ }
471
+ return /* @__PURE__ */ jsx2(
472
+ DiagramInner,
473
+ {
474
+ graph,
475
+ registry,
476
+ visibleIds,
477
+ onSelectNode
478
+ }
479
+ );
480
+ };
481
+ var DiagramInner = ({ graph, registry, visibleIds, onSelectNode }) => {
482
+ const rowCounts = useBulkRowCounts(registry);
483
+ const [focusedNodeId, setFocusedNodeId] = React.useState(null);
484
+ const visibleGraph = React.useMemo(
485
+ () => filterEntityGraphModel(graph, visibleIds),
486
+ [graph, visibleIds]
487
+ );
488
+ React.useEffect(() => {
489
+ if (focusedNodeId && !visibleIds.has(focusedNodeId)) setFocusedNodeId(null);
490
+ }, [focusedNodeId, visibleIds]);
491
+ const focusedConnections = React.useMemo(() => {
492
+ if (!focusedNodeId) {
493
+ return {
494
+ connectedIds: /* @__PURE__ */ new Set(),
495
+ edgeIds: /* @__PURE__ */ new Set(),
496
+ fieldsByNode: /* @__PURE__ */ new Map()
497
+ };
498
+ }
499
+ const connectedIds = /* @__PURE__ */ new Set([focusedNodeId]);
500
+ const edgeIds = /* @__PURE__ */ new Set();
501
+ const fieldsByNode = /* @__PURE__ */ new Map();
502
+ const addField = (nodeId, fieldName) => {
503
+ const fields = fieldsByNode.get(nodeId) ?? /* @__PURE__ */ new Set();
504
+ fields.add(fieldName);
505
+ fieldsByNode.set(nodeId, fields);
506
+ };
507
+ for (const edge of visibleGraph.edges) {
508
+ if (edge.source !== focusedNodeId && edge.target !== focusedNodeId) continue;
509
+ edgeIds.add(edge.id);
510
+ connectedIds.add(edge.source);
511
+ connectedIds.add(edge.target);
512
+ addField(edge.source, edge.sourceField);
513
+ addField(edge.target, edge.targetField);
514
+ }
515
+ return { connectedIds, edgeIds, fieldsByNode };
516
+ }, [focusedNodeId, visibleGraph.edges]);
517
+ const nodes = React.useMemo(
518
+ () => visibleGraph.nodes.map((node) => ({
519
+ id: node.id,
520
+ type: "entity",
521
+ position: node.position,
522
+ data: {
523
+ ...node,
524
+ rowCount: rowCounts[node.id] ?? node.rowCount,
525
+ highlight: focusedNodeId ? {
526
+ focused: node.id === focusedNodeId,
527
+ connected: focusedConnections.connectedIds.has(node.id) && node.id !== focusedNodeId,
528
+ dimmed: !focusedConnections.connectedIds.has(node.id),
529
+ fields: [...focusedConnections.fieldsByNode.get(node.id) ?? []]
530
+ } : void 0
531
+ }
532
+ })),
533
+ [focusedConnections, focusedNodeId, visibleGraph.nodes, rowCounts]
534
+ );
535
+ const edges = React.useMemo(() => {
536
+ const nodeById = new Map(visibleGraph.nodes.map((node) => [node.id, node]));
537
+ const laneCounts = /* @__PURE__ */ new Map();
538
+ return visibleGraph.edges.map((edge) => {
539
+ const source = nodeById.get(edge.source);
540
+ const target = nodeById.get(edge.target);
541
+ const sourceSide = source && target && source.position.x > target.position.x ? "left" : "right";
542
+ const targetSide = sourceSide === "left" ? "right" : "left";
543
+ const laneKey = `${edge.target}:${edge.targetField}:${targetSide}`;
544
+ const lane = laneCounts.get(laneKey) ?? 0;
545
+ laneCounts.set(laneKey, lane + 1);
546
+ const highlighted = !focusedNodeId || focusedConnections.edgeIds.has(edge.id);
547
+ return {
548
+ id: edge.id,
549
+ source: edge.source,
550
+ target: edge.target,
551
+ sourceHandle: `source-${edge.sourceField}-${sourceSide}`,
552
+ targetHandle: `target-${edge.targetField}-${targetSide}`,
553
+ type: "smoothstep",
554
+ pathOptions: { borderRadius: 8, offset: 22 + lane % 5 * 12 },
555
+ markerEnd: {
556
+ type: MarkerType.ArrowClosed,
557
+ color: highlighted ? "#2563eb" : "#64748b"
558
+ },
559
+ style: {
560
+ strokeWidth: focusedNodeId && highlighted ? 2.5 : 1.5,
561
+ stroke: highlighted ? "#2563eb" : "#64748b",
562
+ opacity: highlighted ? 1 : 0.16
563
+ }
564
+ };
565
+ });
566
+ }, [focusedConnections.edgeIds, focusedNodeId, visibleGraph.edges, visibleGraph.nodes]);
567
+ const onNodeClick = React.useCallback(
568
+ (_, node) => {
569
+ const data = node.data;
570
+ const nodeId = entityGraphId(data.cartridge, data.entity);
571
+ setFocusedNodeId((prev) => prev === nodeId ? null : nodeId);
572
+ onSelectNode?.(data.cartridge, data.entity);
573
+ },
574
+ [onSelectNode]
575
+ );
576
+ const onNodeContextMenu = React.useCallback(
577
+ (e, node) => {
578
+ e.preventDefault();
579
+ const data = node.data;
580
+ setFocusedNodeId(entityGraphId(data.cartridge, data.entity));
581
+ onSelectNode?.(data.cartridge, data.entity);
582
+ },
583
+ [onSelectNode]
584
+ );
585
+ const clearFocusedNode = React.useCallback(() => setFocusedNodeId(null), []);
586
+ return /* @__PURE__ */ jsx2("div", { className: "flex h-full min-h-0 w-full flex-col", children: /* @__PURE__ */ jsx2("div", { className: "relative min-h-0 flex-1 bg-zinc-950", children: nodes.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "flex h-full items-center justify-center text-[12px] text-zinc-500", children: "No entities are visible." }) : /* @__PURE__ */ jsx2(ReactFlowProvider, { children: /* @__PURE__ */ jsxs2(
587
+ ReactFlow,
588
+ {
589
+ colorMode: "dark",
590
+ nodes,
591
+ edges,
592
+ nodeTypes,
593
+ fitView: true,
594
+ fitViewOptions: { padding: 0.2 },
595
+ minZoom: 0.1,
596
+ maxZoom: 1.6,
597
+ nodesDraggable: false,
598
+ nodesConnectable: false,
599
+ elementsSelectable: true,
600
+ panOnDrag: true,
601
+ zoomOnScroll: true,
602
+ zoomOnPinch: true,
603
+ selectionOnDrag: false,
604
+ onNodeClick,
605
+ onNodeContextMenu,
606
+ onPaneClick: clearFocusedNode,
607
+ proOptions: { hideAttribution: true },
608
+ children: [
609
+ /* @__PURE__ */ jsx2(FitOnReady, { fitKey: [...visibleIds].sort().join("|") }),
610
+ /* @__PURE__ */ jsx2(Background, { variant: BackgroundVariant.Dots, gap: 16, size: 1, color: "#3f3f46" }),
611
+ /* @__PURE__ */ jsx2(Controls, { showInteractive: false })
612
+ ]
613
+ }
614
+ ) }) }) });
615
+ };
616
+
617
+ export {
618
+ EntityNode,
619
+ cartsFromContracts,
620
+ entityGraphId,
621
+ buildEntityGraphModel,
622
+ filterEntityGraphModel,
623
+ DiagramView
624
+ };
625
+ //# sourceMappingURL=chunk-UPEOXMLZ.js.map