@nbt-dev/devtools 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,2377 @@
1
+ "use client";
2
+
3
+ // src/config.tsx
4
+ import React from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ var DevToolsConfigContext = React.createContext({
7
+ apiBaseUrl: ""
8
+ });
9
+ var DevToolsConfigProvider = ({ apiBaseUrl = "", children }) => {
10
+ const value = React.useMemo(() => ({ apiBaseUrl }), [apiBaseUrl]);
11
+ return /* @__PURE__ */ jsx(DevToolsConfigContext.Provider, { value, children });
12
+ };
13
+ function useDevToolsConfig() {
14
+ return React.useContext(DevToolsConfigContext);
15
+ }
16
+ function wsBaseFrom(apiBaseUrl) {
17
+ if (!apiBaseUrl) {
18
+ const loc = window.location;
19
+ const proto = loc.protocol === "https:" ? "wss:" : "ws:";
20
+ return `${proto}//${loc.host}`;
21
+ }
22
+ return apiBaseUrl.replace(/^http(s?):/, "ws$1:");
23
+ }
24
+
25
+ // src/components/devtools/devtools-context.tsx
26
+ import React2, { useEffect } from "react";
27
+ import { jsx as jsx2 } from "react/jsx-runtime";
28
+ var Ctx = React2.createContext(null);
29
+ var DevToolsProvider = ({ children, defaultActiveTab }) => {
30
+ const [open, setOpen] = React2.useState(false);
31
+ const [dock, setDock] = React2.useState("bottom");
32
+ const [activeTab, setActiveTab] = React2.useState(
33
+ defaultActiveTab ?? null
34
+ );
35
+ const [size, setSize] = React2.useState({ h: 320, w: 480 });
36
+ const [maximized, setMaximized] = React2.useState(false);
37
+ const [dataCart, setDataCart] = React2.useState(null);
38
+ const [dataEntity, setDataEntity] = React2.useState(null);
39
+ const toggle = React2.useCallback(() => setOpen((o) => !o), []);
40
+ const value = React2.useMemo(
41
+ () => ({
42
+ open,
43
+ setOpen,
44
+ toggle,
45
+ dock,
46
+ setDock,
47
+ activeTab,
48
+ setActiveTab,
49
+ size,
50
+ setSize,
51
+ maximized,
52
+ setMaximized,
53
+ dataCart,
54
+ setDataCart,
55
+ dataEntity,
56
+ setDataEntity
57
+ }),
58
+ [open, toggle, dock, activeTab, size, maximized, dataCart, dataEntity]
59
+ );
60
+ useEffect(() => {
61
+ const listen = () => toggle();
62
+ window.addEventListener("devtools-toggle", listen);
63
+ return () => window.removeEventListener("devtools-toggle", listen);
64
+ }, [open, toggle]);
65
+ return /* @__PURE__ */ jsx2(Ctx.Provider, { value, children });
66
+ };
67
+ function useDevTools() {
68
+ const v = React2.useContext(Ctx);
69
+ if (!v) throw new Error("useDevTools must be used inside <DevToolsProvider>");
70
+ return v;
71
+ }
72
+
73
+ // src/components/devtools/dev-tools.tsx
74
+ import React8 from "react";
75
+ import { Maximize2, Minimize2, PanelBottom, PanelRight, X } from "lucide-react";
76
+
77
+ // src/lib/utils.ts
78
+ import { clsx } from "clsx";
79
+ import { twMerge } from "tailwind-merge";
80
+ function cn(...inputs) {
81
+ return twMerge(clsx(inputs));
82
+ }
83
+
84
+ // src/components/devtools/console-tab.tsx
85
+ import React3 from "react";
86
+ import {
87
+ ChevronRight,
88
+ Pause,
89
+ Play,
90
+ Trash2
91
+ } from "lucide-react";
92
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
93
+ var MAX_ENTRIES = 5e3;
94
+ var RECONNECT_MIN_MS = 500;
95
+ var RECONNECT_MAX_MS = 5e3;
96
+ function resolveWsUrl(apiBaseUrl, path) {
97
+ return `${wsBaseFrom(apiBaseUrl)}${path}`;
98
+ }
99
+ function classifyLine(line) {
100
+ if (line.parsed) {
101
+ try {
102
+ const p = JSON.parse(line.raw);
103
+ const lvl = p.level === "info" || p.level === "warn" || p.level === "error" ? p.level : "plain";
104
+ return {
105
+ id: _nextId(),
106
+ raw: line.raw,
107
+ level: lvl,
108
+ parsed: p
109
+ };
110
+ } catch {
111
+ }
112
+ }
113
+ return { id: _nextId(), raw: line.raw, level: "plain", parsed: null };
114
+ }
115
+ var _idCounter = 0;
116
+ function _nextId() {
117
+ _idCounter += 1;
118
+ return _idCounter;
119
+ }
120
+ var LEVEL_BADGE = {
121
+ info: "bg-blue-500/20 text-blue-300 border-blue-400/30",
122
+ warn: "bg-yellow-500/20 text-yellow-300 border-yellow-400/30",
123
+ error: "bg-red-500/20 text-red-300 border-red-400/30",
124
+ plain: "bg-zinc-700/40 text-zinc-300 border-zinc-500/30"
125
+ };
126
+ var ConsoleTab = () => {
127
+ const { apiBaseUrl } = useDevToolsConfig();
128
+ const [entries, setEntries] = React3.useState([]);
129
+ const [paused, setPaused] = React3.useState(false);
130
+ const [filter, setFilter] = React3.useState({
131
+ info: true,
132
+ warn: true,
133
+ error: true,
134
+ plain: true
135
+ });
136
+ const [expanded, setExpanded] = React3.useState({});
137
+ const scrollerRef = React3.useRef(null);
138
+ const stickToBottomRef = React3.useRef(true);
139
+ const pausedRef = React3.useRef(paused);
140
+ pausedRef.current = paused;
141
+ React3.useEffect(() => {
142
+ let alive = true;
143
+ let socket = null;
144
+ let backoff = RECONNECT_MIN_MS;
145
+ let reconnectTimer = null;
146
+ const connect = () => {
147
+ if (!alive) return;
148
+ try {
149
+ socket = new WebSocket(resolveWsUrl(apiBaseUrl, "/_console/logs/ws"));
150
+ } catch {
151
+ scheduleReconnect();
152
+ return;
153
+ }
154
+ socket.onopen = () => {
155
+ backoff = RECONNECT_MIN_MS;
156
+ };
157
+ socket.onmessage = (ev) => {
158
+ if (pausedRef.current) return;
159
+ if (typeof ev.data !== "string") return;
160
+ let frame;
161
+ try {
162
+ frame = JSON.parse(ev.data);
163
+ } catch {
164
+ return;
165
+ }
166
+ if (!frame.lines || frame.lines.length === 0) return;
167
+ const batch = frame.lines.map(classifyLine);
168
+ setEntries((prev) => {
169
+ const next = prev.concat(batch);
170
+ if (next.length > MAX_ENTRIES) {
171
+ return next.slice(next.length - MAX_ENTRIES);
172
+ }
173
+ return next;
174
+ });
175
+ };
176
+ socket.onerror = () => {
177
+ };
178
+ socket.onclose = () => {
179
+ socket = null;
180
+ scheduleReconnect();
181
+ };
182
+ };
183
+ const scheduleReconnect = () => {
184
+ if (!alive) return;
185
+ reconnectTimer = setTimeout(() => {
186
+ backoff = Math.min(backoff * 2, RECONNECT_MAX_MS);
187
+ connect();
188
+ }, backoff);
189
+ };
190
+ connect();
191
+ return () => {
192
+ alive = false;
193
+ if (reconnectTimer) clearTimeout(reconnectTimer);
194
+ if (socket) socket.close();
195
+ };
196
+ }, []);
197
+ const handleScroll = React3.useCallback(() => {
198
+ const el = scrollerRef.current;
199
+ if (!el) return;
200
+ const atBottom = el.scrollHeight - (el.scrollTop + el.clientHeight) < 8;
201
+ stickToBottomRef.current = atBottom;
202
+ }, []);
203
+ const scrollRafRef = React3.useRef(0);
204
+ React3.useEffect(() => {
205
+ if (!stickToBottomRef.current) return;
206
+ if (scrollRafRef.current) return;
207
+ scrollRafRef.current = requestAnimationFrame(() => {
208
+ scrollRafRef.current = 0;
209
+ const el = scrollerRef.current;
210
+ if (!el) return;
211
+ el.scrollTop = el.scrollHeight;
212
+ });
213
+ }, [entries]);
214
+ React3.useEffect(
215
+ () => () => {
216
+ if (scrollRafRef.current) cancelAnimationFrame(scrollRafRef.current);
217
+ },
218
+ []
219
+ );
220
+ const visible = React3.useMemo(
221
+ () => entries.filter((e) => filter[e.level]),
222
+ [entries, filter]
223
+ );
224
+ const toggleFilter = React3.useCallback(
225
+ (k) => setFilter((f) => ({ ...f, [k]: !f[k] })),
226
+ []
227
+ );
228
+ const onRowClick = React3.useCallback((e) => {
229
+ let el = e.target;
230
+ while (el && el !== e.currentTarget) {
231
+ const id = el.dataset.rowId;
232
+ if (id) {
233
+ const n = Number(id);
234
+ if (Number.isFinite(n)) {
235
+ setExpanded((prev) => ({ ...prev, [n]: !prev[n] }));
236
+ }
237
+ return;
238
+ }
239
+ el = el.parentElement;
240
+ }
241
+ }, []);
242
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col", children: [
243
+ /* @__PURE__ */ jsxs("div", { className: "flex h-7 shrink-0 items-center gap-1 border-b border-border bg-background px-1.5 select-none", children: [
244
+ /* @__PURE__ */ jsx3(
245
+ ToolbarButton,
246
+ {
247
+ "aria-label": paused ? "Resume" : "Pause",
248
+ onClick: () => setPaused((p) => !p),
249
+ children: paused ? /* @__PURE__ */ jsx3(Play, { className: "size-3.5" }) : /* @__PURE__ */ jsx3(Pause, { className: "size-3.5" })
250
+ }
251
+ ),
252
+ /* @__PURE__ */ jsx3(
253
+ ToolbarButton,
254
+ {
255
+ "aria-label": "Clear",
256
+ onClick: () => {
257
+ setEntries([]);
258
+ setExpanded({});
259
+ },
260
+ children: /* @__PURE__ */ jsx3(Trash2, { className: "size-3.5" })
261
+ }
262
+ ),
263
+ /* @__PURE__ */ jsx3("div", { className: "mx-1 h-3 w-px bg-border/60" }),
264
+ ["info", "warn", "error", "plain"].map((k) => /* @__PURE__ */ jsx3(
265
+ FilterChip,
266
+ {
267
+ label: k,
268
+ active: filter[k],
269
+ level: k,
270
+ onClick: () => toggleFilter(k)
271
+ },
272
+ k
273
+ )),
274
+ /* @__PURE__ */ jsx3("div", { className: "flex-1" }),
275
+ /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-muted-foreground tabular-nums", children: [
276
+ visible.length,
277
+ " / ",
278
+ entries.length
279
+ ] })
280
+ ] }),
281
+ /* @__PURE__ */ jsx3(
282
+ "div",
283
+ {
284
+ ref: scrollerRef,
285
+ onScroll: handleScroll,
286
+ style: { overflowAnchor: "none" },
287
+ className: "min-h-0 flex-1 overflow-auto font-mono text-[11px] leading-[1.45]",
288
+ children: visible.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "p-3 text-muted-foreground", children: "Waiting for console output\u2026" }) : /* @__PURE__ */ jsx3("ul", { onClick: onRowClick, children: visible.map((e) => /* @__PURE__ */ jsx3(
289
+ LogRow,
290
+ {
291
+ entry: e,
292
+ expanded: !!expanded[e.id]
293
+ },
294
+ e.id
295
+ )) })
296
+ }
297
+ )
298
+ ] });
299
+ };
300
+ var ToolbarButton = ({ className, children, ...rest }) => /* @__PURE__ */ jsx3(
301
+ "button",
302
+ {
303
+ type: "button",
304
+ className: cn(
305
+ "grid size-5 place-items-center rounded-md text-foreground outline-none hover:bg-accent hover:text-accent-foreground",
306
+ className
307
+ ),
308
+ ...rest,
309
+ children
310
+ }
311
+ );
312
+ var FilterChip = ({ label, level, active, onClick }) => /* @__PURE__ */ jsx3(
313
+ "button",
314
+ {
315
+ type: "button",
316
+ onClick,
317
+ "data-active": active,
318
+ className: cn(
319
+ "h-5 rounded-md border px-1.5 text-[11px] leading-none transition-colors",
320
+ "data-[active=false]:opacity-40 data-[active=false]:hover:opacity-70",
321
+ LEVEL_BADGE[level]
322
+ ),
323
+ children: label
324
+ }
325
+ );
326
+ var _LogRow = ({ entry, expanded }) => {
327
+ if (entry.level === "plain" || !entry.parsed) {
328
+ return /* @__PURE__ */ jsx3(
329
+ "li",
330
+ {
331
+ className: "border-b border-border/30 px-2 py-0.5 text-zinc-400 whitespace-pre-wrap break-words",
332
+ style: { contain: "content" },
333
+ children: entry.raw
334
+ }
335
+ );
336
+ }
337
+ const p = entry.parsed;
338
+ const hasStack = Array.isArray(p.stack) && p.stack.length > 0;
339
+ return /* @__PURE__ */ jsxs(
340
+ "li",
341
+ {
342
+ className: "border-b border-border/30",
343
+ style: { contain: "content" },
344
+ "data-row-id": hasStack ? entry.id : void 0,
345
+ children: [
346
+ /* @__PURE__ */ jsxs(
347
+ "div",
348
+ {
349
+ className: cn(
350
+ "flex items-start gap-1.5 px-2 py-0.5",
351
+ hasStack && "cursor-pointer hover:bg-accent/30"
352
+ ),
353
+ children: [
354
+ hasStack ? /* @__PURE__ */ jsx3(
355
+ ChevronRight,
356
+ {
357
+ className: cn(
358
+ "mt-0.5 size-3 shrink-0 transition-transform",
359
+ expanded && "rotate-90"
360
+ )
361
+ }
362
+ ) : /* @__PURE__ */ jsx3("span", { className: "inline-block size-3 shrink-0" }),
363
+ /* @__PURE__ */ jsx3(
364
+ "span",
365
+ {
366
+ className: cn(
367
+ "shrink-0 rounded border px-1 text-[10px] font-medium uppercase tracking-wide",
368
+ LEVEL_BADGE[entry.level]
369
+ ),
370
+ children: entry.level
371
+ }
372
+ ),
373
+ p.ts && /* @__PURE__ */ jsx3("span", { className: "shrink-0 text-zinc-500 tabular-nums", children: p.ts }),
374
+ /* @__PURE__ */ jsx3("span", { className: "break-words text-zinc-200", children: p.msg ?? entry.raw })
375
+ ]
376
+ }
377
+ ),
378
+ hasStack && expanded && /* @__PURE__ */ jsx3("ul", { className: "border-t border-border/20 bg-accent/10 px-7 py-1", children: p.stack.map((f, i) => /* @__PURE__ */ jsxs(
379
+ "li",
380
+ {
381
+ className: "text-zinc-400 whitespace-pre-wrap break-words",
382
+ children: [
383
+ /* @__PURE__ */ jsx3("span", { className: "text-zinc-300", children: f.proc || "<anon>" }),
384
+ /* @__PURE__ */ jsx3("span", { className: "text-zinc-500", children: " @ " }),
385
+ /* @__PURE__ */ jsx3("span", { children: f.file }),
386
+ /* @__PURE__ */ jsx3("span", { className: "text-zinc-500", children: ":" }),
387
+ /* @__PURE__ */ jsx3("span", { className: "tabular-nums text-zinc-300", children: f.line })
388
+ ]
389
+ },
390
+ i
391
+ )) })
392
+ ]
393
+ }
394
+ );
395
+ };
396
+ var LogRow = React3.memo(
397
+ _LogRow,
398
+ (prev, next) => prev.entry === next.entry && prev.expanded === next.expanded
399
+ );
400
+ var console_tab_default = ConsoleTab;
401
+
402
+ // src/components/devtools/network-tab.tsx
403
+ import { jsx as jsx4 } from "react/jsx-runtime";
404
+ var NetworkTab = () => {
405
+ return /* @__PURE__ */ jsx4("div", { className: "p-3 text-muted-foreground", children: "Network placeholder" });
406
+ };
407
+ var network_tab_default = NetworkTab;
408
+
409
+ // src/components/devtools/data-tab.tsx
410
+ import React7 from "react";
411
+ import { Network, Table2 } from "lucide-react";
412
+
413
+ // src/hooks/use-cartridge-info.ts
414
+ import { useEffect as useEffect2, useState } from "react";
415
+ function buildRegistry(contracts) {
416
+ const reg = {};
417
+ for (const c of contracts) {
418
+ const cart = c.cartridge;
419
+ if (!cart || !c.owns) continue;
420
+ const entities = [];
421
+ for (const [name, ent] of Object.entries(c.owns)) {
422
+ const sf = Array.isArray(ent?.searchFields) ? ent.searchFields : [];
423
+ entities.push({
424
+ name,
425
+ route: `/_ws/bulk/${cart}/${name.toLowerCase()}`,
426
+ searchFields: sf
427
+ });
428
+ }
429
+ if (entities.length > 0) reg[cart] = entities;
430
+ }
431
+ return reg;
432
+ }
433
+ function useLiveBulkRegistry() {
434
+ const { apiBaseUrl } = useDevToolsConfig();
435
+ const [registry, setRegistry] = useState({});
436
+ const [loading, setLoading] = useState(true);
437
+ const [error, setError] = useState(null);
438
+ useEffect2(() => {
439
+ const ac = new AbortController();
440
+ let cancelled = false;
441
+ (async () => {
442
+ try {
443
+ const r = await fetch(`${apiBaseUrl}/_console/contracts`, {
444
+ signal: ac.signal,
445
+ credentials: "include"
446
+ });
447
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
448
+ const data = await r.json();
449
+ if (!Array.isArray(data)) throw new Error("malformed contracts response");
450
+ if (cancelled) return;
451
+ setRegistry(buildRegistry(data));
452
+ } catch (e) {
453
+ if (cancelled || e.name === "AbortError") return;
454
+ setError(e instanceof Error ? e.message : String(e));
455
+ } finally {
456
+ if (!cancelled) setLoading(false);
457
+ }
458
+ })();
459
+ return () => {
460
+ cancelled = true;
461
+ ac.abort();
462
+ };
463
+ }, []);
464
+ const carts = Object.keys(registry).sort();
465
+ return { registry, carts, loading, error };
466
+ }
467
+
468
+ // src/hooks/use-bulk-stream.ts
469
+ import React4, {
470
+ createContext,
471
+ useContext,
472
+ useEffect as useEffect3,
473
+ useRef,
474
+ useState as useState2
475
+ } from "react";
476
+
477
+ // src/generated/bulk-protocol.ts
478
+ var FRAME_SCHEMA = 1;
479
+ var FRAME_DATA = 2;
480
+ var FRAME_DATA_END = 3;
481
+ var FRAME_DELTA_INS = 4;
482
+ var FRAME_DELTA_UPD = 5;
483
+ var FRAME_DELTA_DEL = 6;
484
+ var FRAME_SEARCH_RESULT = 7;
485
+ var FRAME_SEARCH_END = 8;
486
+ var FRAME_ERROR = 255;
487
+ var TYPE_U8 = 1;
488
+ var TYPE_U16 = 2;
489
+ var TYPE_U32 = 3;
490
+ var TYPE_U64 = 4;
491
+ var TYPE_S8 = 5;
492
+ var TYPE_S16 = 6;
493
+ var TYPE_S32 = 7;
494
+ var TYPE_S64 = 8;
495
+ var TYPE_BOOL = 9;
496
+ var TYPE_FLOAT32 = 10;
497
+ var TYPE_FLOAT64 = 11;
498
+ var TYPE_STRING = 12;
499
+ var TYPE_DATETIME = 13;
500
+ var TYPE_DOCUMENT = 14;
501
+ function fixedSizeForType(type) {
502
+ switch (type) {
503
+ case TYPE_U8:
504
+ return 1;
505
+ case TYPE_U16:
506
+ return 2;
507
+ case TYPE_U32:
508
+ return 4;
509
+ case TYPE_U64:
510
+ return 8;
511
+ case TYPE_S8:
512
+ return 1;
513
+ case TYPE_S16:
514
+ return 2;
515
+ case TYPE_S32:
516
+ return 4;
517
+ case TYPE_S64:
518
+ return 8;
519
+ case TYPE_BOOL:
520
+ return 1;
521
+ case TYPE_FLOAT32:
522
+ return 4;
523
+ case TYPE_FLOAT64:
524
+ return 8;
525
+ case TYPE_STRING:
526
+ return 0;
527
+ case TYPE_DATETIME:
528
+ return 8;
529
+ case TYPE_DOCUMENT:
530
+ return 0;
531
+ default:
532
+ return 0;
533
+ }
534
+ }
535
+
536
+ // src/components/devtools/data-browser/data-store.ts
537
+ var BulkDataStore = class {
538
+ constructor() {
539
+ this.columns = [];
540
+ this.rows = [];
541
+ this.totalRows = 0;
542
+ this.searchActive = false;
543
+ this._fullRows = [];
544
+ this._fullTotalRows = 0;
545
+ this._idColIndex = -1;
546
+ }
547
+ applySchema(schema) {
548
+ this.columns = schema.columns;
549
+ this.totalRows = schema.totalRows;
550
+ this.rows = [];
551
+ this._idColIndex = schema.columns.findIndex((c) => c.name === "id");
552
+ }
553
+ appendChunk(chunk) {
554
+ for (let i = 0; i < chunk.length; i++) this.rows.push(chunk[i]);
555
+ }
556
+ applyDelta(delta) {
557
+ const target = this.searchActive ? this._fullRows : this.rows;
558
+ if (delta.op === FRAME_DELTA_INS && delta.rowData) {
559
+ target.push(delta.rowData);
560
+ if (this.searchActive) this._fullTotalRows++;
561
+ else this.totalRows++;
562
+ return;
563
+ }
564
+ if (delta.op === FRAME_DELTA_UPD && delta.rowData) {
565
+ const idx = this._findRowById(target, delta.rowData);
566
+ if (idx >= 0) target[idx] = delta.rowData;
567
+ return;
568
+ }
569
+ if (delta.op === FRAME_DELTA_DEL && delta.id !== void 0) {
570
+ const idStr = String(delta.id);
571
+ const idx = this._findRowByIdStr(target, idStr);
572
+ if (idx >= 0) {
573
+ target.splice(idx, 1);
574
+ if (this.searchActive) this._fullTotalRows--;
575
+ else this.totalRows--;
576
+ }
577
+ return;
578
+ }
579
+ }
580
+ enterSearch(schema) {
581
+ if (!this.searchActive) {
582
+ this._fullRows = this.rows;
583
+ this._fullTotalRows = this.totalRows;
584
+ }
585
+ this.searchActive = true;
586
+ this.columns = schema.columns;
587
+ this.totalRows = schema.totalRows;
588
+ this.rows = [];
589
+ }
590
+ exitSearch() {
591
+ if (!this.searchActive) return;
592
+ this.rows = this._fullRows;
593
+ this.totalRows = this._fullTotalRows;
594
+ this._fullRows = [];
595
+ this._fullTotalRows = 0;
596
+ this.searchActive = false;
597
+ }
598
+ getRowCount() {
599
+ return this.rows.length;
600
+ }
601
+ _findRowById(rows, rowData) {
602
+ if (this._idColIndex < 0) return -1;
603
+ const id = rowData[this._idColIndex];
604
+ for (let i = 0; i < rows.length; i++) {
605
+ if (rows[i][this._idColIndex] === id) return i;
606
+ }
607
+ return -1;
608
+ }
609
+ _findRowByIdStr(rows, idStr) {
610
+ if (this._idColIndex < 0) return -1;
611
+ for (let i = 0; i < rows.length; i++) {
612
+ if (rows[i][this._idColIndex] === idStr) return i;
613
+ }
614
+ return -1;
615
+ }
616
+ };
617
+
618
+ // src/components/devtools/data-browser/bulk-decoder.ts
619
+ var textDecoder = new TextDecoder();
620
+ var SID_BYTES = 2;
621
+ var HEAD = 1 + SID_BYTES;
622
+ function epochToDate(epoch) {
623
+ const abs = Math.abs(epoch);
624
+ if (abs < 1e10) return new Date(epoch * 1e3);
625
+ if (abs < 1e13) return new Date(epoch);
626
+ if (abs < 1e16) return new Date(epoch / 1e3);
627
+ return new Date(epoch / 1e6);
628
+ }
629
+ function formatCellValue(view, offset, col) {
630
+ switch (col.type) {
631
+ case TYPE_U8:
632
+ return [String(view.getUint8(offset)), 1];
633
+ case TYPE_S8:
634
+ return [String(view.getInt8(offset)), 1];
635
+ case TYPE_BOOL:
636
+ return [view.getUint8(offset) ? "true" : "false", 1];
637
+ case TYPE_U16:
638
+ return [String(view.getUint16(offset, true)), 2];
639
+ case TYPE_S16:
640
+ return [String(view.getInt16(offset, true)), 2];
641
+ case TYPE_U32:
642
+ return [String(view.getUint32(offset, true)), 4];
643
+ case TYPE_S32:
644
+ return [String(view.getInt32(offset, true)), 4];
645
+ case TYPE_FLOAT32:
646
+ return [String(view.getFloat32(offset, true)), 4];
647
+ case TYPE_U64:
648
+ return [String(view.getBigUint64(offset, true)), 8];
649
+ case TYPE_S64:
650
+ return [String(view.getBigInt64(offset, true)), 8];
651
+ case TYPE_FLOAT64:
652
+ return [String(view.getFloat64(offset, true)), 8];
653
+ case TYPE_DATETIME: {
654
+ const epoch = Number(view.getBigInt64(offset, true));
655
+ if (epoch === 0) return ["", 8];
656
+ return [epochToDate(epoch).toISOString(), 8];
657
+ }
658
+ case TYPE_STRING:
659
+ case TYPE_DOCUMENT: {
660
+ const len = view.getUint32(offset, true);
661
+ const bytes = new Uint8Array(view.buffer, view.byteOffset + offset + 4, len);
662
+ return [textDecoder.decode(bytes), 4 + len];
663
+ }
664
+ default:
665
+ return ["", 0];
666
+ }
667
+ }
668
+ function getFrameType(buf) {
669
+ return new DataView(buf).getUint8(0);
670
+ }
671
+ function getFrameSid(buf) {
672
+ return new DataView(buf).getUint16(1, true);
673
+ }
674
+ function parseSchema(buf) {
675
+ const view = new DataView(buf);
676
+ let offset = 0;
677
+ const frameType = view.getUint8(offset);
678
+ offset += 1;
679
+ if (frameType !== FRAME_SCHEMA) {
680
+ throw new Error(`Expected SCHEMA frame (0x01), got 0x${frameType.toString(16)}`);
681
+ }
682
+ offset += SID_BYTES;
683
+ const totalRows = view.getUint32(offset, true);
684
+ offset += 4;
685
+ const columnCount = view.getUint16(offset, true);
686
+ offset += 2;
687
+ const columns = [];
688
+ for (let i = 0; i < columnCount; i++) {
689
+ const type = view.getUint8(offset);
690
+ offset += 1;
691
+ const nameLen = view.getUint16(offset, true);
692
+ offset += 2;
693
+ const nameBytes = new Uint8Array(buf, offset, nameLen);
694
+ const name = textDecoder.decode(nameBytes);
695
+ offset += nameLen;
696
+ columns.push({ name, type, fixedSize: fixedSizeForType(type) });
697
+ }
698
+ return { totalRows, columns };
699
+ }
700
+ function parseDataChunk(buf, columns) {
701
+ const view = new DataView(buf);
702
+ let offset = 0;
703
+ const frameType = view.getUint8(offset);
704
+ offset += 1;
705
+ if (frameType !== FRAME_DATA && frameType !== FRAME_SEARCH_RESULT) {
706
+ throw new Error(`Expected DATA/SEARCH_RESULT frame, got 0x${frameType.toString(16)}`);
707
+ }
708
+ offset += SID_BYTES;
709
+ const rowCount = view.getUint16(offset, true);
710
+ offset += 2;
711
+ const rows = [];
712
+ for (let r = 0; r < rowCount; r++) {
713
+ const row = [];
714
+ for (let c = 0; c < columns.length; c++) {
715
+ const [value, bytesRead] = formatCellValue(view, offset, columns[c]);
716
+ row.push(value);
717
+ offset += bytesRead;
718
+ }
719
+ rows.push(row);
720
+ }
721
+ return rows;
722
+ }
723
+ function parseDelta(buf, columns) {
724
+ const view = new DataView(buf);
725
+ let offset = 0;
726
+ const op = view.getUint8(offset);
727
+ offset += 1;
728
+ offset += SID_BYTES;
729
+ if (op === FRAME_DELTA_DEL) {
730
+ const len = view.getUint16(offset, true);
731
+ offset += 2;
732
+ const bytes = new Uint8Array(buf, offset, len);
733
+ return { op, id: textDecoder.decode(bytes) };
734
+ }
735
+ if (op === FRAME_DELTA_INS || op === FRAME_DELTA_UPD) {
736
+ const rowData = [];
737
+ for (let c = 0; c < columns.length; c++) {
738
+ const [value, bytesRead] = formatCellValue(view, offset, columns[c]);
739
+ rowData.push(value);
740
+ offset += bytesRead;
741
+ }
742
+ return { op, rowData };
743
+ }
744
+ throw new Error(`Unknown delta op 0x${op.toString(16)}`);
745
+ }
746
+ function parseError(buf) {
747
+ const view = new DataView(buf);
748
+ const len = view.getUint16(HEAD, true);
749
+ const bytes = new Uint8Array(buf, HEAD + 2, len);
750
+ return textDecoder.decode(bytes);
751
+ }
752
+ function encodeSchemaCmd(sid, cart, entity) {
753
+ return JSON.stringify({ cmd: "schema", sid, cart, entity });
754
+ }
755
+ function encodeStreamCmd(sid, cart, entity, opts) {
756
+ return JSON.stringify({ cmd: "sub", sid, cart, entity, ...opts });
757
+ }
758
+ function encodeSearchCmd(sid, query) {
759
+ return JSON.stringify({ cmd: "search", sid, q: query });
760
+ }
761
+ function encodeClearSearchCmd(sid) {
762
+ return JSON.stringify({ cmd: "clear_search", sid });
763
+ }
764
+
765
+ // src/hooks/use-bulk-stream.ts
766
+ var BulkStreamContext = createContext(null);
767
+ function base64UrlEncode(value) {
768
+ if (typeof btoa === "function") {
769
+ return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
770
+ }
771
+ throw new Error("No base64 encoder available for bulk WS auth");
772
+ }
773
+ function bulkAuthProtocols(token) {
774
+ return ["nimbit-bulk", `auth-${base64UrlEncode(token)}`];
775
+ }
776
+ var _wsTokenCache = null;
777
+ var WS_TOKEN_TTL_MS = 6e4;
778
+ async function fetchWsToken(signal, apiBaseUrl) {
779
+ const now = Date.now();
780
+ if (_wsTokenCache && now - _wsTokenCache.at < WS_TOKEN_TTL_MS) {
781
+ return _wsTokenCache.token;
782
+ }
783
+ const r = await fetch(`${apiBaseUrl}/api/auth/session/current`, {
784
+ method: "POST",
785
+ signal,
786
+ credentials: "include",
787
+ headers: { "content-type": "application/json" }
788
+ });
789
+ if (!r.ok) return null;
790
+ const j = await r.json();
791
+ const token = j.session?.token;
792
+ if (!token) return null;
793
+ _wsTokenCache = { token, at: now };
794
+ return token;
795
+ }
796
+ function invalidateWsToken() {
797
+ _wsTokenCache = null;
798
+ }
799
+ function notify(view) {
800
+ view.listeners.forEach((cb) => cb());
801
+ }
802
+ function BulkStreamProvider({ registry, children }) {
803
+ const { apiBaseUrl } = useDevToolsConfig();
804
+ const wsRef = useRef(null);
805
+ const sendQueueRef = useRef([]);
806
+ const sidCounterRef = useRef(1);
807
+ const viewsBySidRef = useRef(/* @__PURE__ */ new Map());
808
+ const viewsByKeyRef = useRef(/* @__PURE__ */ new Map());
809
+ const preloadedRef = useRef(false);
810
+ const [connected, setConnected] = useState2(false);
811
+ const [error, setError] = useState2(null);
812
+ const sendCmd = (cmd) => {
813
+ const ws = wsRef.current;
814
+ if (ws && ws.readyState === WebSocket.OPEN) ws.send(cmd);
815
+ else sendQueueRef.current.push(cmd);
816
+ };
817
+ const flushQueue = () => {
818
+ const ws = wsRef.current;
819
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
820
+ const q = sendQueueRef.current;
821
+ sendQueueRef.current = [];
822
+ for (const cmd of q) ws.send(cmd);
823
+ };
824
+ const getView = (cart, entity) => {
825
+ const entLower = entity.toLowerCase();
826
+ const key = `${cart}/${entLower}`;
827
+ const existing = viewsByKeyRef.current.get(key);
828
+ if (existing) return existing;
829
+ const sid = sidCounterRef.current++;
830
+ const view = {
831
+ sid,
832
+ cart,
833
+ entity: entLower,
834
+ store: new BulkDataStore(),
835
+ columns: [],
836
+ totalRows: 0,
837
+ loadedRows: 0,
838
+ streaming: false,
839
+ error: null,
840
+ streamRequested: false,
841
+ searchStreaming: false,
842
+ onRender: null,
843
+ listeners: /* @__PURE__ */ new Set()
844
+ };
845
+ viewsByKeyRef.current.set(key, view);
846
+ viewsBySidRef.current.set(sid, view);
847
+ return view;
848
+ };
849
+ const ensureStreamed = (view) => {
850
+ if (view.streamRequested) return;
851
+ view.streamRequested = true;
852
+ sendCmd(encodeStreamCmd(view.sid, view.cart, view.entity));
853
+ };
854
+ const handleMessage = (ev) => {
855
+ const buf = ev.data;
856
+ const sid = getFrameSid(buf);
857
+ const view = viewsBySidRef.current.get(sid);
858
+ if (!view) return;
859
+ const ft = getFrameType(buf);
860
+ const store = view.store;
861
+ switch (ft) {
862
+ case FRAME_SCHEMA: {
863
+ const schema = parseSchema(buf);
864
+ if (view.searchStreaming) store.enterSearch(schema);
865
+ else store.applySchema(schema);
866
+ view.columns = schema.columns;
867
+ view.totalRows = schema.totalRows;
868
+ view.loadedRows = 0;
869
+ view.streaming = true;
870
+ notify(view);
871
+ break;
872
+ }
873
+ case FRAME_DATA:
874
+ case FRAME_SEARCH_RESULT: {
875
+ const rows = parseDataChunk(buf, store.columns);
876
+ store.appendChunk(rows);
877
+ view.loadedRows = store.getRowCount();
878
+ view.onRender?.();
879
+ break;
880
+ }
881
+ case FRAME_DATA_END:
882
+ view.streaming = false;
883
+ notify(view);
884
+ break;
885
+ case FRAME_SEARCH_END:
886
+ view.searchStreaming = false;
887
+ view.streaming = false;
888
+ notify(view);
889
+ break;
890
+ case FRAME_DELTA_INS:
891
+ case FRAME_DELTA_UPD:
892
+ case FRAME_DELTA_DEL: {
893
+ const delta = parseDelta(buf, store.columns);
894
+ store.applyDelta(delta);
895
+ view.totalRows = store.totalRows;
896
+ view.loadedRows = store.getRowCount();
897
+ view.onRender?.();
898
+ notify(view);
899
+ break;
900
+ }
901
+ case FRAME_ERROR:
902
+ view.error = parseError(buf);
903
+ view.streaming = false;
904
+ notify(view);
905
+ break;
906
+ }
907
+ };
908
+ useEffect3(() => {
909
+ const ac = new AbortController();
910
+ let cancelled = false;
911
+ let opened = false;
912
+ let ws = null;
913
+ setError(null);
914
+ (async () => {
915
+ let token = null;
916
+ try {
917
+ token = await fetchWsToken(ac.signal, apiBaseUrl);
918
+ } catch {
919
+ if (!cancelled) setError("Failed to obtain WS session token");
920
+ return;
921
+ }
922
+ if (cancelled) return;
923
+ if (!token) {
924
+ setError("No session \u2014 sign in to view data");
925
+ return;
926
+ }
927
+ ws = new WebSocket(`${wsBaseFrom(apiBaseUrl)}/_ws/bulk`, bulkAuthProtocols(token));
928
+ ws.binaryType = "arraybuffer";
929
+ wsRef.current = ws;
930
+ ws.onopen = () => {
931
+ if (cancelled || wsRef.current !== ws) return;
932
+ opened = true;
933
+ setConnected(true);
934
+ setError(null);
935
+ flushQueue();
936
+ };
937
+ ws.onmessage = handleMessage;
938
+ ws.onerror = () => {
939
+ };
940
+ ws.onclose = () => {
941
+ if (cancelled || wsRef.current !== ws) return;
942
+ setConnected(false);
943
+ wsRef.current = null;
944
+ if (!opened) {
945
+ invalidateWsToken();
946
+ setError("WebSocket connection closed");
947
+ }
948
+ };
949
+ })();
950
+ return () => {
951
+ cancelled = true;
952
+ ac.abort();
953
+ if (ws) {
954
+ ws.onopen = ws.onmessage = ws.onerror = ws.onclose = null;
955
+ try {
956
+ ws.close();
957
+ } catch {
958
+ }
959
+ if (wsRef.current === ws) wsRef.current = null;
960
+ }
961
+ };
962
+ }, []);
963
+ useEffect3(() => {
964
+ if (!connected) return;
965
+ if (preloadedRef.current) return;
966
+ const keys = Object.keys(registry);
967
+ if (keys.length === 0) return;
968
+ preloadedRef.current = true;
969
+ for (const cart of keys) {
970
+ for (const ent of registry[cart] ?? []) {
971
+ const view = getView(cart, ent.name);
972
+ sendCmd(encodeSchemaCmd(view.sid, view.cart, view.entity));
973
+ }
974
+ }
975
+ }, [connected, registry]);
976
+ const search = (view, query) => {
977
+ if (!query) {
978
+ view.store.exitSearch();
979
+ view.totalRows = view.store.totalRows;
980
+ view.loadedRows = view.store.getRowCount();
981
+ sendCmd(encodeClearSearchCmd(view.sid));
982
+ notify(view);
983
+ view.onRender?.();
984
+ return;
985
+ }
986
+ view.searchStreaming = true;
987
+ sendCmd(encodeSearchCmd(view.sid, query));
988
+ };
989
+ const clearSearch = (view) => {
990
+ view.store.exitSearch();
991
+ view.totalRows = view.store.totalRows;
992
+ view.loadedRows = view.store.getRowCount();
993
+ sendCmd(encodeClearSearchCmd(view.sid));
994
+ notify(view);
995
+ view.onRender?.();
996
+ };
997
+ const subscribe = (view, cb) => {
998
+ view.listeners.add(cb);
999
+ return () => {
1000
+ view.listeners.delete(cb);
1001
+ };
1002
+ };
1003
+ const ctx = {
1004
+ getView,
1005
+ ensureStreamed,
1006
+ search,
1007
+ clearSearch,
1008
+ subscribe,
1009
+ connected,
1010
+ error
1011
+ };
1012
+ return React4.createElement(BulkStreamContext.Provider, { value: ctx }, children);
1013
+ }
1014
+ function useBulkRowCounts(registry) {
1015
+ const ctx = useContext(BulkStreamContext);
1016
+ if (!ctx) throw new Error("useBulkRowCounts must be used within BulkStreamProvider");
1017
+ const [, force] = useState2(0);
1018
+ const pairs = [];
1019
+ for (const cart of Object.keys(registry)) {
1020
+ for (const ent of registry[cart] ?? []) {
1021
+ pairs.push({ id: `${cart}:${ent.name}`, view: ctx.getView(cart, ent.name) });
1022
+ }
1023
+ }
1024
+ const key = pairs.map((p) => p.id).join("|");
1025
+ useEffect3(() => {
1026
+ const unsubs = pairs.map((p) => ctx.subscribe(p.view, () => force((n) => n + 1)));
1027
+ return () => unsubs.forEach((u) => u());
1028
+ }, [key]);
1029
+ const counts = {};
1030
+ for (const p of pairs) counts[p.id] = p.view.totalRows;
1031
+ return counts;
1032
+ }
1033
+ function useBulkSubscription(cart, entity) {
1034
+ const ctx = useContext(BulkStreamContext);
1035
+ if (!ctx) throw new Error("useBulkSubscription must be used within BulkStreamProvider");
1036
+ const view = ctx.getView(cart, entity);
1037
+ const [, force] = useState2(0);
1038
+ useEffect3(() => {
1039
+ const unsub = ctx.subscribe(view, () => force((n) => n + 1));
1040
+ ctx.ensureStreamed(view);
1041
+ return unsub;
1042
+ }, [view]);
1043
+ return {
1044
+ store: view.store,
1045
+ connected: ctx.connected,
1046
+ streaming: view.streaming,
1047
+ error: view.error ?? ctx.error,
1048
+ columns: view.columns,
1049
+ totalRows: view.totalRows,
1050
+ loadedRows: view.loadedRows,
1051
+ search: (q) => ctx.search(view, q),
1052
+ clearSearch: () => ctx.clearSearch(view),
1053
+ setOnRender: (fn) => {
1054
+ view.onRender = fn;
1055
+ }
1056
+ };
1057
+ }
1058
+
1059
+ // src/components/devtools/data-browser/data-table.tsx
1060
+ import React5 from "react";
1061
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1062
+ var ROW_H = 22;
1063
+ var OVERSCAN = 6;
1064
+ var MIN_COL_W = 80;
1065
+ function colWidth(name) {
1066
+ if (name === "id") return 220;
1067
+ if (name === "createdAt" || name === "updatedAt") return 180;
1068
+ if (name === "email" || name === "phone") return 180;
1069
+ if (name.endsWith("At")) return 180;
1070
+ return Math.max(MIN_COL_W, Math.min(240, name.length * 9 + 24));
1071
+ }
1072
+ var DataTable = ({ cart, entity, searchFields, onSelectRow }) => {
1073
+ const stream = useBulkSubscription(cart, entity);
1074
+ const scrollerRef = React5.useRef(null);
1075
+ const [scrollTop, setScrollTop] = React5.useState(0);
1076
+ const [viewportH, setViewportH] = React5.useState(0);
1077
+ const [query, setQuery] = React5.useState("");
1078
+ const [tick, setTick] = React5.useState(0);
1079
+ React5.useEffect(() => {
1080
+ stream.setOnRender(() => setTick((n) => n + 1));
1081
+ return () => stream.setOnRender(null);
1082
+ }, [stream]);
1083
+ React5.useEffect(() => {
1084
+ const el = scrollerRef.current;
1085
+ if (!el) return;
1086
+ const ro = new ResizeObserver(() => setViewportH(el.clientHeight));
1087
+ ro.observe(el);
1088
+ setViewportH(el.clientHeight);
1089
+ return () => ro.disconnect();
1090
+ }, []);
1091
+ const rows = stream.store.rows;
1092
+ const columns = stream.columns;
1093
+ const totalH = rows.length * ROW_H;
1094
+ const start = Math.max(0, Math.floor(scrollTop / ROW_H) - OVERSCAN);
1095
+ const visibleCount = Math.max(0, Math.ceil(viewportH / ROW_H) + OVERSCAN * 2);
1096
+ const end = Math.min(rows.length, start + visibleCount);
1097
+ const onSearchKey = (e) => {
1098
+ if (e.key === "Enter") {
1099
+ stream.search(query.trim());
1100
+ } else if (e.key === "Escape") {
1101
+ setQuery("");
1102
+ stream.clearSearch();
1103
+ }
1104
+ };
1105
+ const handleRowClick = (rowIdx) => {
1106
+ const r = rows[rowIdx];
1107
+ if (!r) return;
1108
+ const out = {};
1109
+ for (let i = 0; i < columns.length; i++) out[columns[i].name] = r[i] ?? "";
1110
+ onSelectRow(out);
1111
+ };
1112
+ const searchDisabled = searchFields.length === 0;
1113
+ const totalColW = columns.reduce((s, c) => s + colWidth(c.name), 0);
1114
+ return /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-col bg-background", children: [
1115
+ /* @__PURE__ */ jsxs2("div", { className: "flex h-7 shrink-0 items-center gap-2 border-b border-border px-2", children: [
1116
+ /* @__PURE__ */ jsx5(
1117
+ "input",
1118
+ {
1119
+ type: "text",
1120
+ value: query,
1121
+ disabled: searchDisabled,
1122
+ onChange: (e) => setQuery(e.target.value),
1123
+ onKeyDown: onSearchKey,
1124
+ placeholder: searchDisabled ? "Search disabled (no @@search declared)" : `Search ${searchFields.join(", ")} \u23CE`,
1125
+ className: cn(
1126
+ "h-5 flex-1 rounded-sm border border-border bg-background px-2 text-[12px] outline-none",
1127
+ "placeholder:text-muted-foreground/70 focus:border-accent-foreground/30",
1128
+ searchDisabled && "opacity-50 cursor-not-allowed"
1129
+ )
1130
+ }
1131
+ ),
1132
+ /* @__PURE__ */ jsx5("div", { className: "shrink-0 text-[11px] text-muted-foreground tabular-nums", children: stream.error ? /* @__PURE__ */ jsx5("span", { className: "text-red-400", children: stream.error }) : !stream.connected ? /* @__PURE__ */ jsx5("span", { children: "connecting\u2026" }) : /* @__PURE__ */ jsxs2("span", { children: [
1133
+ stream.loadedRows.toLocaleString(),
1134
+ " / ",
1135
+ stream.totalRows.toLocaleString(),
1136
+ " rows",
1137
+ stream.streaming ? " \xB7 streaming" : ""
1138
+ ] }) })
1139
+ ] }),
1140
+ columns.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "flex flex-1 items-center justify-center text-[12px] text-muted-foreground", children: stream.error ? stream.error : "Waiting for schema\u2026" }) : /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1 flex-col", children: [
1141
+ /* @__PURE__ */ jsx5("div", { className: "overflow-hidden border-b border-border bg-muted/30", children: /* @__PURE__ */ jsx5(
1142
+ "div",
1143
+ {
1144
+ className: "flex h-6 select-none text-[11px] uppercase tracking-wider text-muted-foreground",
1145
+ style: {
1146
+ width: totalColW,
1147
+ transform: `translateX(${-(scrollerRef.current?.scrollLeft ?? 0)}px)`
1148
+ },
1149
+ children: columns.map((c) => /* @__PURE__ */ jsx5(
1150
+ "div",
1151
+ {
1152
+ style: { width: colWidth(c.name) },
1153
+ className: "flex items-center overflow-hidden border-r border-border px-2",
1154
+ children: /* @__PURE__ */ jsx5("span", { className: "truncate", children: c.name })
1155
+ },
1156
+ c.name
1157
+ ))
1158
+ }
1159
+ ) }),
1160
+ /* @__PURE__ */ jsx5(
1161
+ "div",
1162
+ {
1163
+ ref: scrollerRef,
1164
+ onScroll: (e) => {
1165
+ setScrollTop(e.target.scrollTop);
1166
+ setTick((n) => n + 1);
1167
+ },
1168
+ className: "relative min-h-0 flex-1 overflow-auto font-mono",
1169
+ children: /* @__PURE__ */ jsx5("div", { style: { height: totalH, width: totalColW, position: "relative" }, children: rows.slice(start, end).map((row, i) => {
1170
+ const rowIdx = start + i;
1171
+ return /* @__PURE__ */ jsx5(
1172
+ "div",
1173
+ {
1174
+ onClick: () => handleRowClick(rowIdx),
1175
+ style: {
1176
+ top: rowIdx * ROW_H,
1177
+ height: ROW_H,
1178
+ width: totalColW
1179
+ },
1180
+ className: cn(
1181
+ "absolute left-0 flex cursor-pointer text-[12px] leading-none",
1182
+ rowIdx % 2 === 0 ? "bg-background" : "bg-muted/20",
1183
+ "hover:bg-accent hover:text-accent-foreground"
1184
+ ),
1185
+ children: columns.map((c, ci) => /* @__PURE__ */ jsx5(
1186
+ "div",
1187
+ {
1188
+ style: { width: colWidth(c.name) },
1189
+ className: "flex items-center overflow-hidden border-r border-border/60 px-2",
1190
+ title: row[ci],
1191
+ children: /* @__PURE__ */ jsx5("span", { className: "truncate", children: row[ci] })
1192
+ },
1193
+ c.name
1194
+ ))
1195
+ },
1196
+ rowIdx
1197
+ );
1198
+ }) })
1199
+ }
1200
+ )
1201
+ ] }),
1202
+ tick < 0 && /* @__PURE__ */ jsx5("span", {})
1203
+ ] });
1204
+ };
1205
+ var data_table_default = DataTable;
1206
+
1207
+ // src/components/devtools/entity-graph/diagram-tab.tsx
1208
+ import React6 from "react";
1209
+ import {
1210
+ Background,
1211
+ BackgroundVariant,
1212
+ Controls,
1213
+ MarkerType,
1214
+ ReactFlow,
1215
+ ReactFlowProvider,
1216
+ useNodesInitialized,
1217
+ useReactFlow
1218
+ } from "@xyflow/react";
1219
+ import { Database as Database2, Eye, EyeOff, GitBranch, Rows3 } from "lucide-react";
1220
+
1221
+ // src/components/devtools/entity-graph/entity-node.tsx
1222
+ import { Handle, Position } from "@xyflow/react";
1223
+ import { Database, FileText, KeyRound, Link2 } from "lucide-react";
1224
+ import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1225
+ function EntityNode({ data, selected }) {
1226
+ const node = data;
1227
+ const highlight = node.highlight;
1228
+ const highlightedFields = new Set(highlight?.fields ?? []);
1229
+ const hiddenHandleClass = "!h-1 !w-1 !border-0 !bg-transparent !opacity-0";
1230
+ return /* @__PURE__ */ jsxs3(
1231
+ "div",
1232
+ {
1233
+ className: [
1234
+ "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",
1235
+ 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",
1236
+ highlight?.dimmed ? "opacity-35" : "",
1237
+ selected && !highlight?.focused ? "ring-2 ring-zinc-100/10" : ""
1238
+ ].join(" "),
1239
+ children: [
1240
+ /* @__PURE__ */ jsxs3("div", { className: "rounded-t-xl border-b border-zinc-700 bg-zinc-800 px-3 py-2", children: [
1241
+ /* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 items-center gap-2", children: [
1242
+ /* @__PURE__ */ jsx6(Database, { className: "h-3.5 w-3.5 shrink-0 text-zinc-400" }),
1243
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate text-[13px] font-semibold leading-none text-zinc-50", children: node.entity }),
1244
+ /* @__PURE__ */ jsx6("span", { className: "shrink-0 rounded bg-zinc-700 px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-zinc-200", children: node.fieldCount })
1245
+ ] }),
1246
+ /* @__PURE__ */ jsx6("div", { className: "mt-1 truncate text-[10px] text-zinc-500", children: node.cartridge })
1247
+ ] }),
1248
+ /* @__PURE__ */ jsx6("div", { className: "bg-zinc-900 py-1", children: node.fields.length === 0 ? /* @__PURE__ */ jsx6("div", { className: "px-3 py-2 text-[10px] text-zinc-500", children: "No fields" }) : node.fields.map((field) => {
1249
+ const type = `${field.type}${field.array ? "[]" : ""}${field.optional ? "?" : ""}`;
1250
+ const isId = field.displayName.toLowerCase() === "id";
1251
+ const isRelation = field.kind === "relation";
1252
+ const isDocument = field.kind === "document";
1253
+ const fieldHighlighted = highlightedFields.has(field.displayName);
1254
+ return /* @__PURE__ */ jsxs3(
1255
+ "div",
1256
+ {
1257
+ className: [
1258
+ "relative grid min-h-[26px] grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-3 py-1 text-zinc-300",
1259
+ fieldHighlighted ? "bg-blue-500/15 text-blue-200" : ""
1260
+ ].join(" "),
1261
+ children: [
1262
+ isId ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1263
+ /* @__PURE__ */ jsx6(
1264
+ Handle,
1265
+ {
1266
+ id: `target-${field.displayName}-left`,
1267
+ type: "target",
1268
+ position: Position.Left,
1269
+ className: `!left-0 ${hiddenHandleClass}`,
1270
+ style: { top: "50%" }
1271
+ }
1272
+ ),
1273
+ /* @__PURE__ */ jsx6(
1274
+ Handle,
1275
+ {
1276
+ id: `target-${field.displayName}-right`,
1277
+ type: "target",
1278
+ position: Position.Right,
1279
+ className: `!right-0 ${hiddenHandleClass}`,
1280
+ style: { top: "50%" }
1281
+ }
1282
+ )
1283
+ ] }) : null,
1284
+ isRelation ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1285
+ /* @__PURE__ */ jsx6(
1286
+ Handle,
1287
+ {
1288
+ id: `source-${field.displayName}-left`,
1289
+ type: "source",
1290
+ position: Position.Left,
1291
+ className: `!left-0 ${hiddenHandleClass}`,
1292
+ style: { top: "50%" }
1293
+ }
1294
+ ),
1295
+ /* @__PURE__ */ jsx6(
1296
+ Handle,
1297
+ {
1298
+ id: `source-${field.displayName}-right`,
1299
+ type: "source",
1300
+ position: Position.Right,
1301
+ className: `!right-0 ${hiddenHandleClass}`,
1302
+ style: { top: "50%" }
1303
+ }
1304
+ )
1305
+ ] }) : null,
1306
+ /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 items-center gap-1.5", children: [
1307
+ isId ? /* @__PURE__ */ jsx6(KeyRound, { className: "h-3 w-3 shrink-0 text-zinc-500" }) : null,
1308
+ isRelation ? /* @__PURE__ */ jsx6(Link2, { className: ["h-3 w-3 shrink-0", fieldHighlighted ? "text-blue-300" : "text-blue-400"].join(" ") }) : null,
1309
+ isDocument ? /* @__PURE__ */ jsx6(FileText, { className: "h-3 w-3 shrink-0 text-zinc-500" }) : null,
1310
+ /* @__PURE__ */ jsx6("span", { className: "truncate", children: field.displayName })
1311
+ ] }),
1312
+ /* @__PURE__ */ jsx6("span", { className: "max-w-[96px] truncate text-right text-zinc-500", children: type })
1313
+ ]
1314
+ },
1315
+ `${field.name}:${field.displayName}`
1316
+ );
1317
+ }) }),
1318
+ /* @__PURE__ */ jsxs3("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: [
1319
+ /* @__PURE__ */ jsxs3("span", { className: "tabular-nums", children: [
1320
+ node.rowCount.toLocaleString(),
1321
+ " rows"
1322
+ ] }),
1323
+ /* @__PURE__ */ jsxs3("span", { className: "tabular-nums", children: [
1324
+ node.relationCount,
1325
+ " rel \xB7 ",
1326
+ node.scalarCount,
1327
+ " scalar"
1328
+ ] })
1329
+ ] })
1330
+ ]
1331
+ }
1332
+ );
1333
+ }
1334
+
1335
+ // src/components/devtools/entity-graph/entity-graph-utils.ts
1336
+ import dagre from "@dagrejs/dagre";
1337
+ var NODE_WIDTH = 260;
1338
+ var MIN_NODE_HEIGHT = 150;
1339
+ var FIELD_ROW_HEIGHT = 26;
1340
+ var COLUMN_WIDTH = 390;
1341
+ function cartsFromContracts(contracts) {
1342
+ const carts = [];
1343
+ for (const c of contracts) {
1344
+ if (!c.cartridge || !c.owns) continue;
1345
+ const entities = [];
1346
+ for (const [name, ent] of Object.entries(c.owns)) {
1347
+ const fields = (ent.fields ?? []).map((f) => ({
1348
+ name: f.name,
1349
+ type: f.type,
1350
+ optional: f.optional,
1351
+ array: f.array,
1352
+ kind: f.kind,
1353
+ target: f.target,
1354
+ targetCart: f.target_cart,
1355
+ relationKind: f.relation_kind,
1356
+ fkField: f.fk_field
1357
+ }));
1358
+ entities.push({ name, fields });
1359
+ }
1360
+ if (entities.length > 0) carts.push({ name: c.cartridge, entities });
1361
+ }
1362
+ return carts;
1363
+ }
1364
+ function entityGraphId(cartridge, entity) {
1365
+ return `${cartridge}:${entity}`;
1366
+ }
1367
+ function fieldKind(field) {
1368
+ return field.kind ?? (field.target ? "relation" : "scalar");
1369
+ }
1370
+ function graphField(field) {
1371
+ const kind = fieldKind(field);
1372
+ return {
1373
+ name: field.name,
1374
+ displayName: kind === "relation" ? field.fkField ?? field.name : field.name,
1375
+ type: kind === "relation" && field.target ? field.target : field.type,
1376
+ kind,
1377
+ optional: field.optional === true,
1378
+ array: field.array === true,
1379
+ target: field.target,
1380
+ targetCart: field.targetCart,
1381
+ relationKind: field.relationKind,
1382
+ fkField: field.fkField
1383
+ };
1384
+ }
1385
+ function makeGraphFields(fields) {
1386
+ const out = fields.map(graphField);
1387
+ if (!out.some((field) => field.displayName.toLowerCase() === "id")) {
1388
+ out.unshift({
1389
+ name: "id",
1390
+ displayName: "id",
1391
+ type: "id",
1392
+ kind: "scalar",
1393
+ optional: false,
1394
+ array: false,
1395
+ implicit: true
1396
+ });
1397
+ }
1398
+ return out;
1399
+ }
1400
+ function normalizeName(value) {
1401
+ return value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
1402
+ }
1403
+ function words(value) {
1404
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).map((part) => part.toLowerCase()).filter(Boolean);
1405
+ }
1406
+ function entityAliases(entity) {
1407
+ const parts = words(entity);
1408
+ const aliases = /* @__PURE__ */ new Set([normalizeName(entity)]);
1409
+ const last = parts.length > 0 ? parts[parts.length - 1] : void 0;
1410
+ if (last) aliases.add(last);
1411
+ if (parts.length > 1 && last) {
1412
+ aliases.add(`${parts.slice(0, -1).map((part) => part[0]).join("")}${last}`);
1413
+ aliases.add(parts.map((part) => part[0]).join(""));
1414
+ }
1415
+ return [...aliases].filter((alias) => alias.length >= 2);
1416
+ }
1417
+ function inferTargetNode(fieldName, nodes) {
1418
+ if (!/id$/i.test(fieldName) || /^id$/i.test(fieldName)) return null;
1419
+ const base = normalizeName(fieldName.replace(/id$/i, ""));
1420
+ if (base.length < 3) return null;
1421
+ const candidates = nodes.flatMap(
1422
+ (node) => entityAliases(node.entity).map((alias) => {
1423
+ const exact = base === alias;
1424
+ const suffix = !exact && alias.length >= 4 && base.endsWith(alias);
1425
+ if (!exact && !suffix) return null;
1426
+ return {
1427
+ node,
1428
+ score: (exact ? 1e3 : 500) + alias.length
1429
+ };
1430
+ })
1431
+ ).filter((candidate) => candidate !== null).sort((a, b) => b.score - a.score || a.node.entity.localeCompare(b.node.entity));
1432
+ const first = candidates[0];
1433
+ if (!first) return null;
1434
+ const second = candidates[1];
1435
+ if (second && first.score === second.score) return null;
1436
+ return first.node;
1437
+ }
1438
+ function markRelationField(node, fieldName, target) {
1439
+ const field = node?.fields.find(
1440
+ (candidate) => candidate.displayName === fieldName || candidate.name === fieldName
1441
+ );
1442
+ if (!field || field.displayName.toLowerCase() === "id") return;
1443
+ field.kind = "relation";
1444
+ field.target = target.entity;
1445
+ field.targetCart = target.cartridge;
1446
+ field.relationKind ?? (field.relationKind = "inferred");
1447
+ field.fkField ?? (field.fkField = fieldName);
1448
+ }
1449
+ function recomputeNodeCounts(nodes) {
1450
+ for (const node of nodes) {
1451
+ node.scalarCount = node.fields.filter((field) => field.kind === "scalar").length;
1452
+ node.relationCount = node.fields.filter((field) => field.kind === "relation").length;
1453
+ node.documentCount = node.fields.filter((field) => field.kind === "document").length;
1454
+ }
1455
+ }
1456
+ function nodeHeight(node) {
1457
+ return Math.max(MIN_NODE_HEIGHT, 74 + node.fields.length * FIELD_ROW_HEIGHT);
1458
+ }
1459
+ function layoutNodes(nodes, edges) {
1460
+ const degree = new Map(nodes.map((n) => [n.id, 0]));
1461
+ for (const edge of edges) {
1462
+ degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1);
1463
+ degree.set(edge.target, (degree.get(edge.target) ?? 0) + 1);
1464
+ }
1465
+ const connected = nodes.filter((n) => (degree.get(n.id) ?? 0) > 0);
1466
+ const isolated = nodes.filter((n) => (degree.get(n.id) ?? 0) === 0);
1467
+ const connectedIds = new Set(connected.map((n) => n.id));
1468
+ let maxX = 0;
1469
+ let maxY = 0;
1470
+ if (connected.length > 0) {
1471
+ const g = new dagre.graphlib.Graph();
1472
+ g.setGraph({ rankdir: "LR", ranksep: 130, nodesep: 36, marginx: 40, marginy: 40 });
1473
+ g.setDefaultEdgeLabel(() => ({}));
1474
+ for (const node of connected) {
1475
+ g.setNode(node.id, { width: NODE_WIDTH, height: nodeHeight(node) });
1476
+ }
1477
+ for (const edge of edges) {
1478
+ if (edge.source === edge.target) continue;
1479
+ if (!connectedIds.has(edge.source) || !connectedIds.has(edge.target)) continue;
1480
+ g.setEdge(edge.source, edge.target);
1481
+ }
1482
+ dagre.layout(g);
1483
+ for (const node of connected) {
1484
+ const p = g.node(node.id);
1485
+ const h = nodeHeight(node);
1486
+ node.position = { x: p.x - NODE_WIDTH / 2, y: p.y - h / 2 };
1487
+ maxX = Math.max(maxX, node.position.x + NODE_WIDTH);
1488
+ maxY = Math.max(maxY, node.position.y + h);
1489
+ }
1490
+ }
1491
+ if (isolated.length > 0) {
1492
+ isolated.sort((a, b) => a.entity.localeCompare(b.entity));
1493
+ const startX = connected.length > 0 ? maxX + COLUMN_WIDTH : 0;
1494
+ const perColumn = Math.max(1, Math.ceil(isolated.length / Math.ceil(isolated.length / 6)));
1495
+ let x = startX;
1496
+ let y = 0;
1497
+ let inColumn = 0;
1498
+ for (const node of isolated) {
1499
+ node.position = { x, y };
1500
+ y += nodeHeight(node) + 48;
1501
+ inColumn += 1;
1502
+ if (inColumn >= perColumn) {
1503
+ inColumn = 0;
1504
+ y = 0;
1505
+ x += COLUMN_WIDTH;
1506
+ }
1507
+ }
1508
+ }
1509
+ }
1510
+ function makeTotals(nodes, edges) {
1511
+ return {
1512
+ entities: nodes.length,
1513
+ relationships: edges.length,
1514
+ rows: nodes.reduce((sum, node) => sum + node.rowCount, 0)
1515
+ };
1516
+ }
1517
+ function buildEntityGraphModel(cartridges) {
1518
+ const sortedCarts = [...cartridges].sort((a, b) => a.name.localeCompare(b.name));
1519
+ const nodes = [];
1520
+ sortedCarts.forEach((cart, cartIndex) => {
1521
+ const entities = [...cart.entities].sort((a, b) => a.name.localeCompare(b.name));
1522
+ let y = 0;
1523
+ entities.forEach((entity) => {
1524
+ const id = entityGraphId(cart.name, entity.name);
1525
+ const fields = entity.fields ?? [];
1526
+ const graphFields = makeGraphFields(fields);
1527
+ nodes.push({
1528
+ id,
1529
+ cartridge: cart.name,
1530
+ entity: entity.name,
1531
+ fields: graphFields,
1532
+ fieldCount: graphFields.length,
1533
+ scalarCount: graphFields.filter((field) => field.kind === "scalar").length,
1534
+ relationCount: graphFields.filter((field) => field.kind === "relation").length,
1535
+ documentCount: graphFields.filter((field) => field.kind === "document").length,
1536
+ rowCount: 0,
1537
+ position: { x: cartIndex * COLUMN_WIDTH, y }
1538
+ });
1539
+ y += Math.max(MIN_NODE_HEIGHT, 74 + graphFields.length * FIELD_ROW_HEIGHT) + 56;
1540
+ });
1541
+ });
1542
+ const nodeIds = new Set(nodes.map((node) => node.id));
1543
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
1544
+ const edges = [];
1545
+ const edgeKeys = /* @__PURE__ */ new Set();
1546
+ for (const cart of sortedCarts) {
1547
+ for (const entity of cart.entities) {
1548
+ const source = entityGraphId(cart.name, entity.name);
1549
+ const sourceNode = nodeById.get(source);
1550
+ for (const field of entity.fields ?? []) {
1551
+ const explicitRelation = fieldKind(field) === "relation" && !!field.target;
1552
+ const inferredTargetNode = explicitRelation ? null : inferTargetNode(field.name, nodes);
1553
+ if (!explicitRelation && !inferredTargetNode) continue;
1554
+ const targetCart = explicitRelation ? field.targetCart ?? cart.name : inferredTargetNode?.cartridge;
1555
+ const targetEntity = explicitRelation ? field.target : inferredTargetNode?.entity;
1556
+ if (!targetCart || !targetEntity) continue;
1557
+ const target = entityGraphId(targetCart, targetEntity);
1558
+ if (!nodeIds.has(source) || !nodeIds.has(target)) continue;
1559
+ const targetNode = nodeById.get(target);
1560
+ const idField = targetNode?.fields.find((candidate) => candidate.displayName.toLowerCase() === "id");
1561
+ const targetField = idField?.displayName ?? "__entity";
1562
+ const sourceGraphField = sourceNode?.fields.find(
1563
+ (candidate) => candidate.name === field.name || candidate.displayName === field.name
1564
+ );
1565
+ const sourceField = sourceGraphField?.displayName ?? field.fkField ?? field.name;
1566
+ const edgeKey = `${source}:${sourceField}->${target}:${targetField}`;
1567
+ if (edgeKeys.has(edgeKey)) continue;
1568
+ edgeKeys.add(edgeKey);
1569
+ if (!explicitRelation && targetNode) markRelationField(sourceNode, sourceField, targetNode);
1570
+ edges.push({
1571
+ id: `${source}:${field.name}->${target}`,
1572
+ source,
1573
+ target,
1574
+ sourceField,
1575
+ targetField,
1576
+ label: field.name,
1577
+ relationKind: field.relationKind ?? (!explicitRelation ? "inferred" : void 0),
1578
+ fkField: field.fkField ?? (!explicitRelation ? field.name : void 0)
1579
+ });
1580
+ }
1581
+ }
1582
+ }
1583
+ recomputeNodeCounts(nodes);
1584
+ layoutNodes(nodes, edges);
1585
+ return {
1586
+ nodes,
1587
+ edges,
1588
+ totals: makeTotals(nodes, edges)
1589
+ };
1590
+ }
1591
+ function filterEntityGraphModel(model, visibleIds) {
1592
+ const nodes = model.nodes.filter((node) => visibleIds.has(node.id));
1593
+ const ids = new Set(nodes.map((node) => node.id));
1594
+ const edges = model.edges.filter((edge) => ids.has(edge.source) && ids.has(edge.target));
1595
+ return {
1596
+ nodes,
1597
+ edges,
1598
+ totals: makeTotals(nodes, edges)
1599
+ };
1600
+ }
1601
+
1602
+ // src/components/devtools/entity-graph/diagram-tab.tsx
1603
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1604
+ var API_BASE = typeof NIMBIT_API_ENDPOINT !== "undefined" && NIMBIT_API_ENDPOINT || "";
1605
+ var STORAGE_KEY = "nimbit:inspector:entity-graph:hidden-carts";
1606
+ var nodeTypes = { entity: EntityNode };
1607
+ var FitOnReady = ({ fitKey }) => {
1608
+ const initialized = useNodesInitialized();
1609
+ const { fitView } = useReactFlow();
1610
+ React6.useEffect(() => {
1611
+ if (initialized) fitView({ padding: 0.25, duration: 200 });
1612
+ }, [initialized, fitKey, fitView]);
1613
+ return null;
1614
+ };
1615
+ function useContracts() {
1616
+ const [contracts, setContracts] = React6.useState([]);
1617
+ const [loading, setLoading] = React6.useState(true);
1618
+ const [error, setError] = React6.useState(null);
1619
+ React6.useEffect(() => {
1620
+ const ac = new AbortController();
1621
+ let cancelled = false;
1622
+ (async () => {
1623
+ try {
1624
+ const r = await fetch(`${API_BASE}/_console/contracts`, {
1625
+ signal: ac.signal,
1626
+ credentials: "include"
1627
+ });
1628
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
1629
+ const data = await r.json();
1630
+ if (!Array.isArray(data)) throw new Error("malformed contracts response");
1631
+ if (!cancelled) setContracts(data);
1632
+ } catch (e) {
1633
+ if (cancelled || e.name === "AbortError") return;
1634
+ setError(e instanceof Error ? e.message : String(e));
1635
+ } finally {
1636
+ if (!cancelled) setLoading(false);
1637
+ }
1638
+ })();
1639
+ return () => {
1640
+ cancelled = true;
1641
+ ac.abort();
1642
+ };
1643
+ }, []);
1644
+ return { contracts, loading, error };
1645
+ }
1646
+ function registryFromContracts(contracts) {
1647
+ const reg = {};
1648
+ for (const c of contracts) {
1649
+ if (!c.cartridge || !c.owns) continue;
1650
+ const entities = Object.keys(c.owns).map((name) => ({
1651
+ name,
1652
+ route: `/_ws/bulk/${c.cartridge}/${name.toLowerCase()}`,
1653
+ searchFields: []
1654
+ }));
1655
+ if (entities.length > 0) reg[c.cartridge] = entities;
1656
+ }
1657
+ return reg;
1658
+ }
1659
+ var DiagramView = () => {
1660
+ const { contracts, loading, error } = useContracts();
1661
+ const graph = React6.useMemo(
1662
+ () => buildEntityGraphModel(cartsFromContracts(contracts)),
1663
+ [contracts]
1664
+ );
1665
+ const registry = React6.useMemo(() => registryFromContracts(contracts), [contracts]);
1666
+ if (graph.nodes.length === 0) {
1667
+ let msg = "No installed cartridges with entities.";
1668
+ if (loading) msg = "Loading entity graph\u2026";
1669
+ else if (error) msg = `Failed to load contracts: ${error}`;
1670
+ return /* @__PURE__ */ jsx7("div", { className: "p-3 text-[12px] text-muted-foreground", children: msg });
1671
+ }
1672
+ return /* @__PURE__ */ jsx7(DiagramInner, { graph, registry });
1673
+ };
1674
+ var DiagramInner = ({
1675
+ graph,
1676
+ registry
1677
+ }) => {
1678
+ const rowCounts = useBulkRowCounts(registry);
1679
+ const [hiddenCarts, setHiddenCarts] = React6.useState(() => {
1680
+ if (typeof window === "undefined") return /* @__PURE__ */ new Set();
1681
+ try {
1682
+ const raw = window.localStorage.getItem(STORAGE_KEY);
1683
+ const saved = raw ? JSON.parse(raw) : [];
1684
+ return new Set(Array.isArray(saved) ? saved.filter((n) => typeof n === "string") : []);
1685
+ } catch {
1686
+ return /* @__PURE__ */ new Set();
1687
+ }
1688
+ });
1689
+ const [focusedNodeId, setFocusedNodeId] = React6.useState(null);
1690
+ React6.useEffect(() => {
1691
+ if (typeof window === "undefined") return;
1692
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify([...hiddenCarts]));
1693
+ }, [hiddenCarts]);
1694
+ const cartGroups = React6.useMemo(() => {
1695
+ const counts = /* @__PURE__ */ new Map();
1696
+ for (const node of graph.nodes) counts.set(node.cartridge, (counts.get(node.cartridge) ?? 0) + 1);
1697
+ return [...counts.entries()].map(([name, total]) => ({ name, total })).sort((a, b) => a.name.localeCompare(b.name));
1698
+ }, [graph.nodes]);
1699
+ const visibleIds = React6.useMemo(
1700
+ () => new Set(graph.nodes.filter((n) => !hiddenCarts.has(n.cartridge)).map((n) => n.id)),
1701
+ [graph.nodes, hiddenCarts]
1702
+ );
1703
+ const visibleGraph = React6.useMemo(
1704
+ () => filterEntityGraphModel(graph, visibleIds),
1705
+ [graph, visibleIds]
1706
+ );
1707
+ React6.useEffect(() => {
1708
+ if (focusedNodeId && !visibleIds.has(focusedNodeId)) setFocusedNodeId(null);
1709
+ }, [focusedNodeId, visibleIds]);
1710
+ const focusedConnections = React6.useMemo(() => {
1711
+ if (!focusedNodeId) {
1712
+ return {
1713
+ connectedIds: /* @__PURE__ */ new Set(),
1714
+ edgeIds: /* @__PURE__ */ new Set(),
1715
+ fieldsByNode: /* @__PURE__ */ new Map()
1716
+ };
1717
+ }
1718
+ const connectedIds = /* @__PURE__ */ new Set([focusedNodeId]);
1719
+ const edgeIds = /* @__PURE__ */ new Set();
1720
+ const fieldsByNode = /* @__PURE__ */ new Map();
1721
+ const addField = (nodeId, fieldName) => {
1722
+ const fields = fieldsByNode.get(nodeId) ?? /* @__PURE__ */ new Set();
1723
+ fields.add(fieldName);
1724
+ fieldsByNode.set(nodeId, fields);
1725
+ };
1726
+ for (const edge of visibleGraph.edges) {
1727
+ if (edge.source !== focusedNodeId && edge.target !== focusedNodeId) continue;
1728
+ edgeIds.add(edge.id);
1729
+ connectedIds.add(edge.source);
1730
+ connectedIds.add(edge.target);
1731
+ addField(edge.source, edge.sourceField);
1732
+ addField(edge.target, edge.targetField);
1733
+ }
1734
+ return { connectedIds, edgeIds, fieldsByNode };
1735
+ }, [focusedNodeId, visibleGraph.edges]);
1736
+ const liveRows = React6.useMemo(
1737
+ () => visibleGraph.nodes.reduce((sum, n) => sum + (rowCounts[n.id] ?? 0), 0),
1738
+ [visibleGraph.nodes, rowCounts]
1739
+ );
1740
+ const nodes = React6.useMemo(
1741
+ () => visibleGraph.nodes.map((node) => ({
1742
+ id: node.id,
1743
+ type: "entity",
1744
+ position: node.position,
1745
+ data: {
1746
+ ...node,
1747
+ rowCount: rowCounts[node.id] ?? node.rowCount,
1748
+ highlight: focusedNodeId ? {
1749
+ focused: node.id === focusedNodeId,
1750
+ connected: focusedConnections.connectedIds.has(node.id) && node.id !== focusedNodeId,
1751
+ dimmed: !focusedConnections.connectedIds.has(node.id),
1752
+ fields: [...focusedConnections.fieldsByNode.get(node.id) ?? []]
1753
+ } : void 0
1754
+ }
1755
+ })),
1756
+ [focusedConnections, focusedNodeId, visibleGraph.nodes, rowCounts]
1757
+ );
1758
+ const edges = React6.useMemo(() => {
1759
+ const nodeById = new Map(visibleGraph.nodes.map((node) => [node.id, node]));
1760
+ const laneCounts = /* @__PURE__ */ new Map();
1761
+ return visibleGraph.edges.map((edge) => {
1762
+ const source = nodeById.get(edge.source);
1763
+ const target = nodeById.get(edge.target);
1764
+ const sourceSide = source && target && source.position.x > target.position.x ? "left" : "right";
1765
+ const targetSide = sourceSide === "left" ? "right" : "left";
1766
+ const laneKey = `${edge.target}:${edge.targetField}:${targetSide}`;
1767
+ const lane = laneCounts.get(laneKey) ?? 0;
1768
+ laneCounts.set(laneKey, lane + 1);
1769
+ const highlighted = !focusedNodeId || focusedConnections.edgeIds.has(edge.id);
1770
+ return {
1771
+ id: edge.id,
1772
+ source: edge.source,
1773
+ target: edge.target,
1774
+ sourceHandle: `source-${edge.sourceField}-${sourceSide}`,
1775
+ targetHandle: `target-${edge.targetField}-${targetSide}`,
1776
+ type: "smoothstep",
1777
+ pathOptions: { borderRadius: 8, offset: 22 + lane % 5 * 12 },
1778
+ markerEnd: {
1779
+ type: MarkerType.ArrowClosed,
1780
+ color: highlighted ? "#2563eb" : "#64748b"
1781
+ },
1782
+ style: {
1783
+ strokeWidth: focusedNodeId && highlighted ? 2.5 : 1.5,
1784
+ stroke: highlighted ? "#2563eb" : "#64748b",
1785
+ opacity: highlighted ? 1 : 0.16
1786
+ }
1787
+ };
1788
+ });
1789
+ }, [focusedConnections.edgeIds, focusedNodeId, visibleGraph.edges, visibleGraph.nodes]);
1790
+ const toggleCart = React6.useCallback((name) => {
1791
+ setHiddenCarts((prev) => {
1792
+ const next = new Set(prev);
1793
+ if (next.has(name)) next.delete(name);
1794
+ else next.add(name);
1795
+ return next;
1796
+ });
1797
+ }, []);
1798
+ const onNodeClick = React6.useCallback((_, node) => {
1799
+ const data = node.data;
1800
+ const nodeId = entityGraphId(data.cartridge, data.entity);
1801
+ setFocusedNodeId((prev) => prev === nodeId ? null : nodeId);
1802
+ }, []);
1803
+ const clearFocusedNode = React6.useCallback(() => setFocusedNodeId(null), []);
1804
+ return /* @__PURE__ */ jsxs4("div", { className: "flex h-full min-h-0 w-full flex-col", children: [
1805
+ /* @__PURE__ */ jsxs4("div", { className: "flex h-7 shrink-0 items-center gap-3 border-b border-border bg-background px-2 text-[11px] text-muted-foreground select-none", children: [
1806
+ /* @__PURE__ */ jsxs4("span", { className: "inline-flex items-center gap-1 text-foreground", children: [
1807
+ /* @__PURE__ */ jsx7(GitBranch, { className: "h-3.5 w-3.5" }),
1808
+ "Entity Graph"
1809
+ ] }),
1810
+ /* @__PURE__ */ jsxs4("span", { className: "ml-auto inline-flex items-center gap-1", children: [
1811
+ /* @__PURE__ */ jsx7(Database2, { className: "h-3 w-3" }),
1812
+ visibleGraph.totals.entities,
1813
+ " entities"
1814
+ ] }),
1815
+ /* @__PURE__ */ jsxs4("span", { className: "inline-flex items-center gap-1", children: [
1816
+ /* @__PURE__ */ jsx7(GitBranch, { className: "h-3 w-3" }),
1817
+ visibleGraph.totals.relationships,
1818
+ " relationships"
1819
+ ] }),
1820
+ /* @__PURE__ */ jsxs4("span", { className: "inline-flex items-center gap-1", children: [
1821
+ /* @__PURE__ */ jsx7(Rows3, { className: "h-3 w-3" }),
1822
+ liveRows.toLocaleString(),
1823
+ " rows"
1824
+ ] })
1825
+ ] }),
1826
+ /* @__PURE__ */ jsxs4("div", { className: "relative min-h-0 flex-1 bg-zinc-950", children: [
1827
+ cartGroups.length > 0 ? /* @__PURE__ */ jsx7("div", { className: "pointer-events-none absolute left-2 top-2 z-10 max-h-[calc(100%-1rem)]", children: /* @__PURE__ */ jsx7("div", { className: "pointer-events-auto flex max-h-full flex-col gap-0.5 overflow-y-auto rounded-md border border-zinc-700 bg-zinc-900/95 p-1 shadow-sm backdrop-blur", children: cartGroups.map((g) => {
1828
+ const hidden = hiddenCarts.has(g.name);
1829
+ return /* @__PURE__ */ jsxs4(
1830
+ "button",
1831
+ {
1832
+ type: "button",
1833
+ onClick: () => toggleCart(g.name),
1834
+ className: cn(
1835
+ "flex items-center gap-2 rounded px-2 py-1 text-left font-mono text-[11px] hover:bg-zinc-800",
1836
+ hidden ? "text-zinc-500" : "text-zinc-100"
1837
+ ),
1838
+ title: hidden ? `Show ${g.name}` : `Hide ${g.name}`,
1839
+ children: [
1840
+ hidden ? /* @__PURE__ */ jsx7(EyeOff, { className: "h-3.5 w-3.5 shrink-0" }) : /* @__PURE__ */ jsx7(Eye, { className: "h-3.5 w-3.5 shrink-0 text-orange-400" }),
1841
+ /* @__PURE__ */ jsx7("span", { className: "min-w-0 flex-1 truncate", children: g.name }),
1842
+ /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-[10px] tabular-nums text-zinc-500", children: g.total })
1843
+ ]
1844
+ },
1845
+ g.name
1846
+ );
1847
+ }) }) }) : null,
1848
+ nodes.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "flex h-full items-center justify-center text-[12px] text-zinc-500", children: "No entities are visible." }) : /* @__PURE__ */ jsx7(ReactFlowProvider, { children: /* @__PURE__ */ jsxs4(
1849
+ ReactFlow,
1850
+ {
1851
+ colorMode: "dark",
1852
+ nodes,
1853
+ edges,
1854
+ nodeTypes,
1855
+ fitView: true,
1856
+ fitViewOptions: { padding: 0.2 },
1857
+ minZoom: 0.1,
1858
+ maxZoom: 1.6,
1859
+ nodesDraggable: false,
1860
+ nodesConnectable: false,
1861
+ elementsSelectable: true,
1862
+ panOnDrag: true,
1863
+ panOnScroll: true,
1864
+ zoomOnScroll: false,
1865
+ zoomOnPinch: true,
1866
+ selectionOnDrag: false,
1867
+ onNodeClick,
1868
+ onPaneClick: clearFocusedNode,
1869
+ proOptions: { hideAttribution: true },
1870
+ children: [
1871
+ /* @__PURE__ */ jsx7(FitOnReady, { fitKey: [...visibleIds].sort().join("|") }),
1872
+ /* @__PURE__ */ jsx7(Background, { variant: BackgroundVariant.Dots, gap: 16, size: 1, color: "#3f3f46" }),
1873
+ /* @__PURE__ */ jsx7(Controls, { showInteractive: false })
1874
+ ]
1875
+ }
1876
+ ) })
1877
+ ] })
1878
+ ] });
1879
+ };
1880
+
1881
+ // src/components/ui/sheet.tsx
1882
+ import { Dialog as SheetPrimitive } from "radix-ui";
1883
+
1884
+ // src/components/ui/button.tsx
1885
+ import { cva } from "class-variance-authority";
1886
+ import { Slot } from "radix-ui";
1887
+ import { jsx as jsx8 } from "react/jsx-runtime";
1888
+ var buttonVariants = cva(
1889
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1890
+ {
1891
+ variants: {
1892
+ variant: {
1893
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
1894
+ outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
1895
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
1896
+ ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
1897
+ destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
1898
+ link: "text-primary underline-offset-4 hover:underline"
1899
+ },
1900
+ size: {
1901
+ default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
1902
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
1903
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
1904
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
1905
+ icon: "size-8",
1906
+ "icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
1907
+ "icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
1908
+ "icon-lg": "size-9"
1909
+ }
1910
+ },
1911
+ defaultVariants: {
1912
+ variant: "default",
1913
+ size: "default"
1914
+ }
1915
+ }
1916
+ );
1917
+ function Button({
1918
+ className,
1919
+ variant = "default",
1920
+ size = "default",
1921
+ asChild = false,
1922
+ ...props
1923
+ }) {
1924
+ const Comp = asChild ? Slot.Root : "button";
1925
+ return /* @__PURE__ */ jsx8(
1926
+ Comp,
1927
+ {
1928
+ "data-slot": "button",
1929
+ "data-variant": variant,
1930
+ "data-size": size,
1931
+ className: cn(buttonVariants({ variant, size, className })),
1932
+ ...props
1933
+ }
1934
+ );
1935
+ }
1936
+
1937
+ // src/components/ui/sheet.tsx
1938
+ import { XIcon } from "lucide-react";
1939
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1940
+ function Sheet({ ...props }) {
1941
+ return /* @__PURE__ */ jsx9(SheetPrimitive.Root, { "data-slot": "sheet", ...props });
1942
+ }
1943
+ function SheetPortal({
1944
+ ...props
1945
+ }) {
1946
+ return /* @__PURE__ */ jsx9(SheetPrimitive.Portal, { "data-slot": "sheet-portal", ...props });
1947
+ }
1948
+ function SheetOverlay({
1949
+ className,
1950
+ ...props
1951
+ }) {
1952
+ return /* @__PURE__ */ jsx9(
1953
+ SheetPrimitive.Overlay,
1954
+ {
1955
+ "data-slot": "sheet-overlay",
1956
+ className: cn(
1957
+ "nimbit-devtools dark fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
1958
+ className
1959
+ ),
1960
+ ...props
1961
+ }
1962
+ );
1963
+ }
1964
+ function SheetContent({
1965
+ className,
1966
+ children,
1967
+ side = "right",
1968
+ showCloseButton = true,
1969
+ ...props
1970
+ }) {
1971
+ return /* @__PURE__ */ jsxs5(SheetPortal, { children: [
1972
+ /* @__PURE__ */ jsx9(SheetOverlay, {}),
1973
+ /* @__PURE__ */ jsxs5(
1974
+ SheetPrimitive.Content,
1975
+ {
1976
+ "data-slot": "sheet-content",
1977
+ "data-side": side,
1978
+ className: cn(
1979
+ "nimbit-devtools dark fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
1980
+ className
1981
+ ),
1982
+ ...props,
1983
+ children: [
1984
+ children,
1985
+ showCloseButton && /* @__PURE__ */ jsx9(SheetPrimitive.Close, { "data-slot": "sheet-close", asChild: true, children: /* @__PURE__ */ jsxs5(
1986
+ Button,
1987
+ {
1988
+ variant: "ghost",
1989
+ className: "absolute top-3 right-3",
1990
+ size: "icon-sm",
1991
+ children: [
1992
+ /* @__PURE__ */ jsx9(
1993
+ XIcon,
1994
+ {}
1995
+ ),
1996
+ /* @__PURE__ */ jsx9("span", { className: "sr-only", children: "Close" })
1997
+ ]
1998
+ }
1999
+ ) })
2000
+ ]
2001
+ }
2002
+ )
2003
+ ] });
2004
+ }
2005
+ function SheetHeader({ className, ...props }) {
2006
+ return /* @__PURE__ */ jsx9(
2007
+ "div",
2008
+ {
2009
+ "data-slot": "sheet-header",
2010
+ className: cn("flex flex-col gap-0.5 p-4", className),
2011
+ ...props
2012
+ }
2013
+ );
2014
+ }
2015
+ function SheetTitle({
2016
+ className,
2017
+ ...props
2018
+ }) {
2019
+ return /* @__PURE__ */ jsx9(
2020
+ SheetPrimitive.Title,
2021
+ {
2022
+ "data-slot": "sheet-title",
2023
+ className: cn(
2024
+ "text-base font-medium text-foreground",
2025
+ className
2026
+ ),
2027
+ ...props
2028
+ }
2029
+ );
2030
+ }
2031
+ function SheetDescription({
2032
+ className,
2033
+ ...props
2034
+ }) {
2035
+ return /* @__PURE__ */ jsx9(
2036
+ SheetPrimitive.Description,
2037
+ {
2038
+ "data-slot": "sheet-description",
2039
+ className: cn("text-sm text-muted-foreground", className),
2040
+ ...props
2041
+ }
2042
+ );
2043
+ }
2044
+
2045
+ // src/components/devtools/data-tab.tsx
2046
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2047
+ var DataTab = () => {
2048
+ const { dataCart, setDataCart, dataEntity, setDataEntity } = useDevTools();
2049
+ const [detail, setDetail] = React7.useState(null);
2050
+ const [view, setView] = React7.useState("table");
2051
+ const { registry, carts, loading, error } = useLiveBulkRegistry();
2052
+ const activeCart = dataCart && registry[dataCart] ? dataCart : carts[0] ?? null;
2053
+ const entities = activeCart ? registry[activeCart] ?? [] : [];
2054
+ const activeEntity = entities.find((e) => e.name === dataEntity) ?? entities[0] ?? null;
2055
+ React7.useEffect(() => {
2056
+ if (activeCart && activeCart !== dataCart) setDataCart(activeCart);
2057
+ if (activeEntity && activeEntity.name !== dataEntity) setDataEntity(activeEntity.name);
2058
+ }, [activeCart, activeEntity, dataCart, dataEntity, setDataCart, setDataEntity]);
2059
+ if (carts.length === 0) {
2060
+ let msg = "No installed cartridges with entities.";
2061
+ if (loading) msg = "Loading cartridges\u2026";
2062
+ else if (error) msg = `Failed to load cartridges: ${error}`;
2063
+ return /* @__PURE__ */ jsx10("div", { className: "p-3 text-[12px] text-muted-foreground", children: msg });
2064
+ }
2065
+ return /* @__PURE__ */ jsx10(BulkStreamProvider, { registry, children: /* @__PURE__ */ jsxs6("div", { className: "flex h-full flex-col", children: [
2066
+ /* @__PURE__ */ jsxs6("div", { className: "flex h-7 shrink-0 items-center gap-1 overflow-x-auto border-b border-border bg-background px-1 select-none", children: [
2067
+ /* @__PURE__ */ jsxs6("div", { className: "flex shrink-0 items-center gap-0.5 pr-1", children: [
2068
+ /* @__PURE__ */ jsx10(ViewBtn, { active: view === "table", onClick: () => setView("table"), label: "Table", children: /* @__PURE__ */ jsx10(Table2, { className: "size-3" }) }),
2069
+ /* @__PURE__ */ jsx10(ViewBtn, { active: view === "diagram", onClick: () => setView("diagram"), label: "Graph", children: /* @__PURE__ */ jsx10(Network, { className: "size-3" }) })
2070
+ ] }),
2071
+ view === "table" ? /* @__PURE__ */ jsxs6(Fragment2, { children: [
2072
+ /* @__PURE__ */ jsx10("div", { className: "h-4 w-px shrink-0 bg-border" }),
2073
+ carts.map((cart) => {
2074
+ const isActive = cart === activeCart;
2075
+ return /* @__PURE__ */ jsx10(
2076
+ "button",
2077
+ {
2078
+ type: "button",
2079
+ "data-active": isActive,
2080
+ onClick: () => {
2081
+ setDataCart(cart);
2082
+ const first = registry[cart]?.[0];
2083
+ setDataEntity(first ? first.name : null);
2084
+ },
2085
+ className: cn(
2086
+ "h-5 shrink-0 rounded-full px-2.5 text-[11px] leading-none outline-none transition-colors",
2087
+ "border border-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground",
2088
+ "data-[active=true]:border-border data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
2089
+ ),
2090
+ children: cart
2091
+ },
2092
+ cart
2093
+ );
2094
+ })
2095
+ ] }) : null
2096
+ ] }),
2097
+ view === "diagram" ? /* @__PURE__ */ jsx10("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx10(DiagramView, {}) }) : /* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 flex-1", children: [
2098
+ /* @__PURE__ */ jsx10("div", { className: "flex w-44 shrink-0 flex-col overflow-y-auto border-r border-border bg-muted/10 py-1", children: entities.length === 0 ? /* @__PURE__ */ jsx10("div", { className: "px-2 py-1 text-[11px] text-muted-foreground", children: "No entities" }) : entities.map((e) => {
2099
+ const isActive = e.name === activeEntity?.name;
2100
+ return /* @__PURE__ */ jsx10(
2101
+ "button",
2102
+ {
2103
+ type: "button",
2104
+ "data-active": isActive,
2105
+ onClick: () => setDataEntity(e.name),
2106
+ className: cn(
2107
+ "h-6 shrink-0 text-left px-2 text-[12px] leading-none outline-none transition-colors",
2108
+ "text-foreground hover:bg-accent hover:text-accent-foreground",
2109
+ "data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
2110
+ ),
2111
+ children: e.name
2112
+ },
2113
+ e.name
2114
+ );
2115
+ }) }),
2116
+ /* @__PURE__ */ jsx10("div", { className: "min-h-0 flex-1", children: activeEntity ? /* @__PURE__ */ jsx10(
2117
+ data_table_default,
2118
+ {
2119
+ cart: activeCart,
2120
+ entity: activeEntity.name,
2121
+ searchFields: activeEntity.searchFields,
2122
+ onSelectRow: (row) => setDetail(row)
2123
+ },
2124
+ `${activeCart}:${activeEntity.name}`
2125
+ ) : /* @__PURE__ */ jsx10("div", { className: "p-3 text-[12px] text-muted-foreground", children: "Pick an entity." }) })
2126
+ ] }),
2127
+ /* @__PURE__ */ jsx10(Sheet, { open: !!detail, onOpenChange: (o) => !o && setDetail(null), children: /* @__PURE__ */ jsxs6(SheetContent, { side: "right", className: "w-[480px] sm:max-w-[480px]", children: [
2128
+ /* @__PURE__ */ jsxs6(SheetHeader, { children: [
2129
+ /* @__PURE__ */ jsxs6(SheetTitle, { className: "text-[13px]", children: [
2130
+ activeCart,
2131
+ ".",
2132
+ activeEntity?.name,
2133
+ detail?.id ? /* @__PURE__ */ jsx10("span", { className: "ml-2 font-mono text-[11px] text-muted-foreground", children: detail.id }) : null
2134
+ ] }),
2135
+ /* @__PURE__ */ jsx10(SheetDescription, { className: "text-[11px]", children: "Row detail" })
2136
+ ] }),
2137
+ /* @__PURE__ */ jsx10("div", { className: "min-h-0 flex-1 overflow-auto px-4 pb-4", children: detail ? /* @__PURE__ */ jsx10("table", { className: "w-full text-[12px]", children: /* @__PURE__ */ jsx10("tbody", { children: Object.entries(detail).map(([k, v]) => /* @__PURE__ */ jsxs6("tr", { className: "border-b border-border/40 align-top", children: [
2138
+ /* @__PURE__ */ jsx10("td", { className: "w-32 py-1 pr-2 font-mono text-[11px] text-muted-foreground", children: k }),
2139
+ /* @__PURE__ */ jsx10("td", { className: "break-all py-1 font-mono", children: v || /* @__PURE__ */ jsx10("span", { className: "text-muted-foreground/60", children: "\u2205" }) })
2140
+ ] }, k)) }) }) : null })
2141
+ ] }) })
2142
+ ] }) });
2143
+ };
2144
+ var ViewBtn = ({ active, onClick, label, children }) => /* @__PURE__ */ jsxs6(
2145
+ "button",
2146
+ {
2147
+ type: "button",
2148
+ "data-active": active,
2149
+ onClick,
2150
+ title: label,
2151
+ className: cn(
2152
+ "inline-flex h-5 shrink-0 items-center gap-1 rounded-md px-1.5 text-[11px] leading-none outline-none transition-colors",
2153
+ "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
2154
+ "data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
2155
+ ),
2156
+ children: [
2157
+ children,
2158
+ label
2159
+ ]
2160
+ }
2161
+ );
2162
+ var data_tab_default = DataTab;
2163
+
2164
+ // src/components/devtools/settings-tab.tsx
2165
+ import { Fragment as Fragment3, jsx as jsx11 } from "react/jsx-runtime";
2166
+ var SettingsTab = () => {
2167
+ return /* @__PURE__ */ jsx11(Fragment3, {});
2168
+ };
2169
+ var settings_tab_default = SettingsTab;
2170
+
2171
+ // src/components/devtools/dev-tools.tsx
2172
+ import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
2173
+ var MIN_H = 120;
2174
+ var MIN_W = 240;
2175
+ var DEV_TOOLS_TABS = [
2176
+ { id: "console", title: "Console", render: () => /* @__PURE__ */ jsx12(console_tab_default, {}) },
2177
+ { id: "network", title: "Network", render: () => /* @__PURE__ */ jsx12(network_tab_default, {}) },
2178
+ { id: "data", title: "Data", render: () => /* @__PURE__ */ jsx12(data_tab_default, {}) },
2179
+ { id: "settings", title: "Settings", render: () => /* @__PURE__ */ jsx12(settings_tab_default, {}) }
2180
+ ];
2181
+ var DevTools = () => {
2182
+ const {
2183
+ open,
2184
+ setOpen,
2185
+ dock,
2186
+ setDock,
2187
+ activeTab,
2188
+ setActiveTab,
2189
+ size,
2190
+ setSize,
2191
+ maximized,
2192
+ setMaximized
2193
+ } = useDevTools();
2194
+ const panelRef = React8.useRef(null);
2195
+ React8.useEffect(() => {
2196
+ if (activeTab) return;
2197
+ if (DEV_TOOLS_TABS.length > 0) setActiveTab(DEV_TOOLS_TABS[0].id);
2198
+ }, [activeTab, setActiveTab]);
2199
+ const startResize = React8.useCallback(
2200
+ (e) => {
2201
+ e.preventDefault();
2202
+ const panel = panelRef.current;
2203
+ if (!panel) return;
2204
+ const parent = panel.parentElement;
2205
+ if (!parent) return;
2206
+ const handle = e.currentTarget;
2207
+ handle.setPointerCapture(e.pointerId);
2208
+ const parentRect = parent.getBoundingClientRect();
2209
+ const startX = e.clientX;
2210
+ const startY = e.clientY;
2211
+ const startH = panel.offsetHeight;
2212
+ const startW = panel.offsetWidth;
2213
+ const isBottom = dock === "bottom";
2214
+ let pendingNext = null;
2215
+ let lastApplied = isBottom ? startH : startW;
2216
+ let rafId = 0;
2217
+ const flush = () => {
2218
+ rafId = 0;
2219
+ if (pendingNext == null) return;
2220
+ const v = pendingNext;
2221
+ pendingNext = null;
2222
+ lastApplied = v;
2223
+ if (isBottom) panel.style.height = `${v}px`;
2224
+ else panel.style.width = `${v}px`;
2225
+ };
2226
+ const onMove = (ev) => {
2227
+ ev.preventDefault();
2228
+ if (isBottom) {
2229
+ const dy = startY - ev.clientY;
2230
+ pendingNext = Math.max(
2231
+ MIN_H,
2232
+ Math.min(parentRect.height - 24, startH + dy)
2233
+ );
2234
+ } else {
2235
+ const dx = startX - ev.clientX;
2236
+ pendingNext = Math.max(
2237
+ MIN_W,
2238
+ Math.min(parentRect.width - 24, startW + dx)
2239
+ );
2240
+ }
2241
+ if (!rafId) rafId = requestAnimationFrame(flush);
2242
+ };
2243
+ const onUp = () => {
2244
+ if (rafId) cancelAnimationFrame(rafId);
2245
+ flush();
2246
+ setSize(
2247
+ (prev) => isBottom ? prev.h === lastApplied ? prev : { ...prev, h: lastApplied } : prev.w === lastApplied ? prev : { ...prev, w: lastApplied }
2248
+ );
2249
+ handle.removeEventListener("pointermove", onMove);
2250
+ handle.removeEventListener("pointerup", onUp);
2251
+ handle.removeEventListener("pointercancel", onUp);
2252
+ try {
2253
+ handle.releasePointerCapture(e.pointerId);
2254
+ } catch {
2255
+ }
2256
+ document.body.style.userSelect = "";
2257
+ document.body.style.cursor = "";
2258
+ };
2259
+ document.body.style.userSelect = "none";
2260
+ document.body.style.cursor = dock === "bottom" ? "ns-resize" : "ew-resize";
2261
+ handle.addEventListener("pointermove", onMove);
2262
+ handle.addEventListener("pointerup", onUp);
2263
+ handle.addEventListener("pointercancel", onUp);
2264
+ },
2265
+ [dock, setSize]
2266
+ );
2267
+ if (!open) return null;
2268
+ const active = DEV_TOOLS_TABS.find((t) => t.id === activeTab) ?? DEV_TOOLS_TABS[0];
2269
+ const positionClass = maximized ? "inset-0" : dock === "bottom" ? "inset-x-0 bottom-0" : "inset-y-0 right-0";
2270
+ const sizeStyle = maximized ? {} : dock === "bottom" ? { height: size.h } : { width: size.w };
2271
+ return /* @__PURE__ */ jsxs7(
2272
+ "div",
2273
+ {
2274
+ ref: panelRef,
2275
+ style: sizeStyle,
2276
+ className: cn(
2277
+ "nimbit-devtools dark absolute z-40 flex flex-col border-border bg-background text-foreground text-[12px] shadow-lg",
2278
+ positionClass,
2279
+ dock === "bottom" ? "border-t" : "border-l"
2280
+ ),
2281
+ onMouseDown: (e) => e.stopPropagation(),
2282
+ children: [
2283
+ !maximized && /* @__PURE__ */ jsx12(
2284
+ "div",
2285
+ {
2286
+ onPointerDown: startResize,
2287
+ style: { touchAction: "none" },
2288
+ className: cn(
2289
+ "absolute z-10 bg-transparent hover:bg-accent/40",
2290
+ dock === "bottom" ? "left-0 right-0 -top-px h-1.5 cursor-ns-resize" : "top-0 bottom-0 -left-px w-1.5 cursor-ew-resize"
2291
+ )
2292
+ }
2293
+ ),
2294
+ /* @__PURE__ */ jsxs7("div", { className: "flex h-7 shrink-0 items-center border-b border-border bg-background pl-1 pr-1 select-none", children: [
2295
+ /* @__PURE__ */ jsx12("div", { className: "flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto", children: DEV_TOOLS_TABS.map((tab) => {
2296
+ const isActive = tab.id === active?.id;
2297
+ return /* @__PURE__ */ jsx12(
2298
+ "button",
2299
+ {
2300
+ type: "button",
2301
+ "data-active": isActive,
2302
+ onClick: () => setActiveTab(tab.id),
2303
+ className: cn(
2304
+ "h-5 shrink-0 rounded-md px-2 text-[12px] leading-none outline-none transition-colors",
2305
+ "text-foreground hover:bg-accent hover:text-accent-foreground",
2306
+ "data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
2307
+ ),
2308
+ children: /* @__PURE__ */ jsxs7("span", { className: "inline-flex items-center gap-1", children: [
2309
+ tab.icon,
2310
+ tab.title
2311
+ ] })
2312
+ },
2313
+ tab.id
2314
+ );
2315
+ }) }),
2316
+ /* @__PURE__ */ jsxs7("div", { className: "flex shrink-0 items-center gap-0.5 pl-2", children: [
2317
+ /* @__PURE__ */ jsx12(
2318
+ IconBtn,
2319
+ {
2320
+ label: dock === "bottom" ? "Dock right" : "Dock bottom",
2321
+ onClick: () => setDock(dock === "bottom" ? "right" : "bottom"),
2322
+ children: dock === "bottom" ? /* @__PURE__ */ jsx12(PanelRight, { className: "size-3.5" }) : /* @__PURE__ */ jsx12(PanelBottom, { className: "size-3.5" })
2323
+ }
2324
+ ),
2325
+ /* @__PURE__ */ jsx12(
2326
+ IconBtn,
2327
+ {
2328
+ label: maximized ? "Restore" : "Maximize",
2329
+ onClick: () => setMaximized(!maximized),
2330
+ children: maximized ? /* @__PURE__ */ jsx12(Minimize2, { className: "size-3.5" }) : /* @__PURE__ */ jsx12(Maximize2, { className: "size-3.5" })
2331
+ }
2332
+ ),
2333
+ /* @__PURE__ */ jsx12(IconBtn, { label: "Close", onClick: () => setOpen(false), children: /* @__PURE__ */ jsx12(X, { className: "size-3.5" }) })
2334
+ ] })
2335
+ ] }),
2336
+ /* @__PURE__ */ jsx12("div", { className: "min-h-0 flex-1 overflow-auto", children: active ? active.render() : null })
2337
+ ]
2338
+ }
2339
+ );
2340
+ };
2341
+ var IconBtn = ({ label, className, children, ...rest }) => /* @__PURE__ */ jsx12(
2342
+ "button",
2343
+ {
2344
+ type: "button",
2345
+ "aria-label": label,
2346
+ title: label,
2347
+ className: cn(
2348
+ "grid size-5 place-items-center rounded-md text-foreground outline-none hover:bg-accent hover:text-accent-foreground",
2349
+ className
2350
+ ),
2351
+ ...rest,
2352
+ children
2353
+ }
2354
+ );
2355
+ var dev_tools_default = DevTools;
2356
+
2357
+ // src/index.tsx
2358
+ import { jsx as jsx13 } from "react/jsx-runtime";
2359
+ var NimbitDevTools = ({
2360
+ apiBaseUrl = "",
2361
+ defaultActiveTab
2362
+ }) => /* @__PURE__ */ jsx13(DevToolsConfigProvider, { apiBaseUrl, children: /* @__PURE__ */ jsx13(DevToolsProvider, { defaultActiveTab, children: /* @__PURE__ */ jsx13(dev_tools_default, {}) }) });
2363
+ var index_default = NimbitDevTools;
2364
+ function toggleDevTools() {
2365
+ window.dispatchEvent(new CustomEvent("devtools-toggle"));
2366
+ }
2367
+ export {
2368
+ dev_tools_default as DevTools,
2369
+ DevToolsConfigProvider,
2370
+ DevToolsProvider,
2371
+ NimbitDevTools,
2372
+ index_default as default,
2373
+ toggleDevTools,
2374
+ useDevTools,
2375
+ useDevToolsConfig
2376
+ };
2377
+ //# sourceMappingURL=index.js.map