@nbt-dev/devtools 0.0.1 → 0.0.3

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 CHANGED
@@ -26,6 +26,24 @@ function wsBaseFrom(apiBaseUrl) {
26
26
  import React2, { useEffect } from "react";
27
27
  import { jsx as jsx2 } from "react/jsx-runtime";
28
28
  var Ctx = React2.createContext(null);
29
+ var STORAGE_KEY = "nimbit-devtools:v1";
30
+ var DEFAULT_FILTER = {
31
+ info: true,
32
+ warn: true,
33
+ error: true,
34
+ plain: true
35
+ };
36
+ function loadPersisted() {
37
+ if (typeof window === "undefined") return {};
38
+ try {
39
+ const raw = window.localStorage.getItem(STORAGE_KEY);
40
+ if (!raw) return {};
41
+ const parsed = JSON.parse(raw);
42
+ return typeof parsed === "object" && parsed ? parsed : {};
43
+ } catch {
44
+ return {};
45
+ }
46
+ }
29
47
  var DevToolsProvider = ({ children, defaultActiveTab }) => {
30
48
  const [open, setOpen] = React2.useState(false);
31
49
  const [dock, setDock] = React2.useState("bottom");
@@ -36,7 +54,87 @@ var DevToolsProvider = ({ children, defaultActiveTab }) => {
36
54
  const [maximized, setMaximized] = React2.useState(false);
37
55
  const [dataCart, setDataCart] = React2.useState(null);
38
56
  const [dataEntity, setDataEntity] = React2.useState(null);
57
+ const [view, setView] = React2.useState("table");
58
+ const [graphCarts, setGraphCarts] = React2.useState(null);
59
+ const [hiddenEntities, setHiddenEntities] = React2.useState(
60
+ () => /* @__PURE__ */ new Set()
61
+ );
62
+ const [graphLeftW, setGraphLeftW] = React2.useState(176);
63
+ const [graphRightW, setGraphRightW] = React2.useState(440);
64
+ const [consoleFilter, setConsoleFilter] = React2.useState(DEFAULT_FILTER);
65
+ const [consolePaused, setConsolePaused] = React2.useState(false);
66
+ const [sourcesCart, setSourcesCart] = React2.useState(null);
67
+ const [sourcesFile, setSourcesFile] = React2.useState(null);
68
+ const [sourcesTreeW, setSourcesTreeW] = React2.useState(200);
69
+ const [hydrated, setHydrated] = React2.useState(false);
39
70
  const toggle = React2.useCallback(() => setOpen((o) => !o), []);
71
+ useEffect(() => {
72
+ const saved = loadPersisted();
73
+ if (saved.open != null) setOpen(saved.open);
74
+ if (saved.dock != null) setDock(saved.dock);
75
+ if (saved.activeTab != null) setActiveTab(saved.activeTab);
76
+ if (saved.size != null) setSize(saved.size);
77
+ if (saved.maximized != null) setMaximized(saved.maximized);
78
+ if (saved.dataCart != null) setDataCart(saved.dataCart);
79
+ if (saved.dataEntity != null) setDataEntity(saved.dataEntity);
80
+ if (saved.view != null) setView(saved.view);
81
+ if (saved.graphCarts != null) setGraphCarts(new Set(saved.graphCarts));
82
+ if (saved.hiddenEntities != null)
83
+ setHiddenEntities(new Set(saved.hiddenEntities));
84
+ if (saved.graphLeftW != null) setGraphLeftW(saved.graphLeftW);
85
+ if (saved.graphRightW != null) setGraphRightW(saved.graphRightW);
86
+ if (saved.consoleFilter != null) setConsoleFilter(saved.consoleFilter);
87
+ if (saved.consolePaused != null) setConsolePaused(saved.consolePaused);
88
+ if (saved.sourcesCart != null) setSourcesCart(saved.sourcesCart);
89
+ if (saved.sourcesFile != null) setSourcesFile(saved.sourcesFile);
90
+ if (saved.sourcesTreeW != null) setSourcesTreeW(saved.sourcesTreeW);
91
+ setHydrated(true);
92
+ }, []);
93
+ useEffect(() => {
94
+ if (typeof window === "undefined" || !hydrated) return;
95
+ const snapshot = {
96
+ open,
97
+ dock,
98
+ activeTab,
99
+ size,
100
+ maximized,
101
+ dataCart,
102
+ dataEntity,
103
+ view,
104
+ graphCarts: graphCarts ? [...graphCarts] : null,
105
+ hiddenEntities: [...hiddenEntities],
106
+ graphLeftW,
107
+ graphRightW,
108
+ consoleFilter,
109
+ consolePaused,
110
+ sourcesCart,
111
+ sourcesFile,
112
+ sourcesTreeW
113
+ };
114
+ try {
115
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
116
+ } catch {
117
+ }
118
+ }, [
119
+ hydrated,
120
+ open,
121
+ dock,
122
+ activeTab,
123
+ size,
124
+ maximized,
125
+ dataCart,
126
+ dataEntity,
127
+ view,
128
+ graphCarts,
129
+ hiddenEntities,
130
+ graphLeftW,
131
+ graphRightW,
132
+ consoleFilter,
133
+ consolePaused,
134
+ sourcesCart,
135
+ sourcesFile,
136
+ sourcesTreeW
137
+ ]);
40
138
  const value = React2.useMemo(
41
139
  () => ({
42
140
  open,
@@ -53,15 +151,64 @@ var DevToolsProvider = ({ children, defaultActiveTab }) => {
53
151
  dataCart,
54
152
  setDataCart,
55
153
  dataEntity,
56
- setDataEntity
154
+ setDataEntity,
155
+ view,
156
+ setView,
157
+ graphCarts,
158
+ setGraphCarts,
159
+ hiddenEntities,
160
+ setHiddenEntities,
161
+ graphLeftW,
162
+ setGraphLeftW,
163
+ graphRightW,
164
+ setGraphRightW,
165
+ consoleFilter,
166
+ setConsoleFilter,
167
+ consolePaused,
168
+ setConsolePaused,
169
+ sourcesCart,
170
+ setSourcesCart,
171
+ sourcesFile,
172
+ setSourcesFile,
173
+ sourcesTreeW,
174
+ setSourcesTreeW
57
175
  }),
58
- [open, toggle, dock, activeTab, size, maximized, dataCart, dataEntity]
176
+ [
177
+ open,
178
+ toggle,
179
+ dock,
180
+ activeTab,
181
+ size,
182
+ maximized,
183
+ dataCart,
184
+ dataEntity,
185
+ view,
186
+ graphCarts,
187
+ hiddenEntities,
188
+ graphLeftW,
189
+ graphRightW,
190
+ consoleFilter,
191
+ consolePaused,
192
+ sourcesCart,
193
+ sourcesFile,
194
+ sourcesTreeW
195
+ ]
59
196
  );
60
197
  useEffect(() => {
61
- const listen = () => toggle();
62
- window.addEventListener("devtools-toggle", listen);
63
- return () => window.removeEventListener("devtools-toggle", listen);
64
- }, [open, toggle]);
198
+ const onKey = (e) => {
199
+ if (e.ctrlKey && (e.key === "F12" || e.code === "F12")) {
200
+ e.preventDefault();
201
+ toggle();
202
+ }
203
+ };
204
+ const onEvent = () => toggle();
205
+ window.addEventListener("keydown", onKey);
206
+ window.addEventListener("devtools-toggle", onEvent);
207
+ return () => {
208
+ window.removeEventListener("keydown", onKey);
209
+ window.removeEventListener("devtools-toggle", onEvent);
210
+ };
211
+ }, [toggle]);
65
212
  return /* @__PURE__ */ jsx2(Ctx.Provider, { value, children });
66
213
  };
67
214
  function useDevTools() {
@@ -71,8 +218,9 @@ function useDevTools() {
71
218
  }
72
219
 
73
220
  // src/components/devtools/dev-tools.tsx
74
- import React8 from "react";
75
- import { Maximize2, Minimize2, PanelBottom, PanelRight, X } from "lucide-react";
221
+ import React12 from "react";
222
+ import { createPortal as createPortal2 } from "react-dom";
223
+ import { Maximize2, Minimize2, PanelBottom, PanelRight, X as X2 } from "lucide-react";
76
224
 
77
225
  // src/lib/utils.ts
78
226
  import { clsx } from "clsx";
@@ -125,14 +273,13 @@ var LEVEL_BADGE = {
125
273
  };
126
274
  var ConsoleTab = () => {
127
275
  const { apiBaseUrl } = useDevToolsConfig();
276
+ const {
277
+ consoleFilter: filter,
278
+ setConsoleFilter: setFilter,
279
+ consolePaused: paused,
280
+ setConsolePaused: setPaused
281
+ } = useDevTools();
128
282
  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
283
  const [expanded, setExpanded] = React3.useState({});
137
284
  const scrollerRef = React3.useRef(null);
138
285
  const stickToBottomRef = React3.useRef(true);
@@ -193,7 +340,7 @@ var ConsoleTab = () => {
193
340
  if (reconnectTimer) clearTimeout(reconnectTimer);
194
341
  if (socket) socket.close();
195
342
  };
196
- }, []);
343
+ }, [apiBaseUrl]);
197
344
  const handleScroll = React3.useCallback(() => {
198
345
  const el = scrollerRef.current;
199
346
  if (!el) return;
@@ -407,8 +554,8 @@ var NetworkTab = () => {
407
554
  var network_tab_default = NetworkTab;
408
555
 
409
556
  // src/components/devtools/data-tab.tsx
410
- import React7 from "react";
411
- import { Network, Table2 } from "lucide-react";
557
+ import React8 from "react";
558
+ import { Network, Table2, X } from "lucide-react";
412
559
 
413
560
  // src/hooks/use-cartridge-info.ts
414
561
  import { useEffect as useEffect2, useState } from "react";
@@ -438,6 +585,8 @@ function useLiveBulkRegistry() {
438
585
  useEffect2(() => {
439
586
  const ac = new AbortController();
440
587
  let cancelled = false;
588
+ setLoading(true);
589
+ setError(null);
441
590
  (async () => {
442
591
  try {
443
592
  const r = await fetch(`${apiBaseUrl}/_console/contracts`, {
@@ -460,7 +609,7 @@ function useLiveBulkRegistry() {
460
609
  cancelled = true;
461
610
  ac.abort();
462
611
  };
463
- }, []);
612
+ }, [apiBaseUrl]);
464
613
  const carts = Object.keys(registry).sort();
465
614
  return { registry, carts, loading, error };
466
615
  }
@@ -481,8 +630,6 @@ var FRAME_DATA_END = 3;
481
630
  var FRAME_DELTA_INS = 4;
482
631
  var FRAME_DELTA_UPD = 5;
483
632
  var FRAME_DELTA_DEL = 6;
484
- var FRAME_SEARCH_RESULT = 7;
485
- var FRAME_SEARCH_END = 8;
486
633
  var FRAME_ERROR = 255;
487
634
  var TYPE_U8 = 1;
488
635
  var TYPE_U16 = 2;
@@ -543,27 +690,39 @@ var BulkDataStore = class {
543
690
  this._fullRows = [];
544
691
  this._fullTotalRows = 0;
545
692
  this._idColIndex = -1;
693
+ this._query = "";
546
694
  }
547
695
  applySchema(schema) {
548
696
  this.columns = schema.columns;
549
697
  this.totalRows = schema.totalRows;
550
698
  this.rows = [];
699
+ this._fullRows = [];
700
+ this._fullTotalRows = schema.totalRows;
701
+ this.searchActive = false;
702
+ this._query = "";
551
703
  this._idColIndex = schema.columns.findIndex((c) => c.name === "id");
552
704
  }
553
705
  appendChunk(chunk) {
554
- for (let i = 0; i < chunk.length; i++) this.rows.push(chunk[i]);
706
+ for (let i = 0; i < chunk.length; i++) {
707
+ const row = chunk[i];
708
+ this._fullRows.push(row);
709
+ if (!this.searchActive) this.rows.push(row);
710
+ }
555
711
  }
556
712
  applyDelta(delta) {
557
- const target = this.searchActive ? this._fullRows : this.rows;
713
+ const target = this._fullRows;
558
714
  if (delta.op === FRAME_DELTA_INS && delta.rowData) {
559
715
  target.push(delta.rowData);
560
- if (this.searchActive) this._fullTotalRows++;
561
- else this.totalRows++;
716
+ this._fullTotalRows++;
717
+ this.totalRows++;
718
+ if (!this.searchActive) this.rows = this._fullRows;
719
+ else this._applySearch();
562
720
  return;
563
721
  }
564
722
  if (delta.op === FRAME_DELTA_UPD && delta.rowData) {
565
723
  const idx = this._findRowById(target, delta.rowData);
566
724
  if (idx >= 0) target[idx] = delta.rowData;
725
+ if (this.searchActive) this._applySearch();
567
726
  return;
568
727
  }
569
728
  if (delta.op === FRAME_DELTA_DEL && delta.id !== void 0) {
@@ -571,29 +730,30 @@ var BulkDataStore = class {
571
730
  const idx = this._findRowByIdStr(target, idStr);
572
731
  if (idx >= 0) {
573
732
  target.splice(idx, 1);
574
- if (this.searchActive) this._fullTotalRows--;
575
- else this.totalRows--;
733
+ this._fullTotalRows--;
734
+ this.totalRows--;
735
+ if (!this.searchActive) this.rows = this._fullRows;
736
+ else this._applySearch();
576
737
  }
577
738
  return;
578
739
  }
579
740
  }
580
- enterSearch(schema) {
581
- if (!this.searchActive) {
582
- this._fullRows = this.rows;
583
- this._fullTotalRows = this.totalRows;
741
+ search(query) {
742
+ const q = query.trim().toLowerCase();
743
+ if (!q) {
744
+ this.exitSearch();
745
+ return;
584
746
  }
585
747
  this.searchActive = true;
586
- this.columns = schema.columns;
587
- this.totalRows = schema.totalRows;
588
- this.rows = [];
748
+ this._query = q;
749
+ this._applySearch();
589
750
  }
590
751
  exitSearch() {
591
752
  if (!this.searchActive) return;
592
753
  this.rows = this._fullRows;
593
754
  this.totalRows = this._fullTotalRows;
594
- this._fullRows = [];
595
- this._fullTotalRows = 0;
596
755
  this.searchActive = false;
756
+ this._query = "";
597
757
  }
598
758
  getRowCount() {
599
759
  return this.rows.length;
@@ -613,6 +773,12 @@ var BulkDataStore = class {
613
773
  }
614
774
  return -1;
615
775
  }
776
+ _applySearch() {
777
+ this.rows = this._fullRows.filter(
778
+ (row) => row.some((value) => value.toLowerCase().includes(this._query))
779
+ );
780
+ this.totalRows = this.rows.length;
781
+ }
616
782
  };
617
783
 
618
784
  // src/components/devtools/data-browser/bulk-decoder.ts
@@ -702,8 +868,8 @@ function parseDataChunk(buf, columns) {
702
868
  let offset = 0;
703
869
  const frameType = view.getUint8(offset);
704
870
  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)}`);
871
+ if (frameType !== FRAME_DATA) {
872
+ throw new Error(`Expected DATA frame, got 0x${frameType.toString(16)}`);
707
873
  }
708
874
  offset += SID_BYTES;
709
875
  const rowCount = view.getUint16(offset, true);
@@ -749,18 +915,9 @@ function parseError(buf) {
749
915
  const bytes = new Uint8Array(buf, HEAD + 2, len);
750
916
  return textDecoder.decode(bytes);
751
917
  }
752
- function encodeSchemaCmd(sid, cart, entity) {
753
- return JSON.stringify({ cmd: "schema", sid, cart, entity });
754
- }
755
918
  function encodeStreamCmd(sid, cart, entity, opts) {
756
919
  return JSON.stringify({ cmd: "sub", sid, cart, entity, ...opts });
757
920
  }
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
921
 
765
922
  // src/hooks/use-bulk-stream.ts
766
923
  var BulkStreamContext = createContext(null);
@@ -777,7 +934,7 @@ var _wsTokenCache = null;
777
934
  var WS_TOKEN_TTL_MS = 6e4;
778
935
  async function fetchWsToken(signal, apiBaseUrl) {
779
936
  const now = Date.now();
780
- if (_wsTokenCache && now - _wsTokenCache.at < WS_TOKEN_TTL_MS) {
937
+ if (_wsTokenCache?.apiBaseUrl === apiBaseUrl && now - _wsTokenCache.at < WS_TOKEN_TTL_MS) {
781
938
  return _wsTokenCache.token;
782
939
  }
783
940
  const r = await fetch(`${apiBaseUrl}/api/auth/session/current`, {
@@ -790,7 +947,7 @@ async function fetchWsToken(signal, apiBaseUrl) {
790
947
  const j = await r.json();
791
948
  const token = j.session?.token;
792
949
  if (!token) return null;
793
- _wsTokenCache = { token, at: now };
950
+ _wsTokenCache = { apiBaseUrl, token, at: now };
794
951
  return token;
795
952
  }
796
953
  function invalidateWsToken() {
@@ -838,7 +995,6 @@ function BulkStreamProvider({ registry, children }) {
838
995
  streaming: false,
839
996
  error: null,
840
997
  streamRequested: false,
841
- searchStreaming: false,
842
998
  onRender: null,
843
999
  listeners: /* @__PURE__ */ new Set()
844
1000
  };
@@ -861,8 +1017,7 @@ function BulkStreamProvider({ registry, children }) {
861
1017
  switch (ft) {
862
1018
  case FRAME_SCHEMA: {
863
1019
  const schema = parseSchema(buf);
864
- if (view.searchStreaming) store.enterSearch(schema);
865
- else store.applySchema(schema);
1020
+ store.applySchema(schema);
866
1021
  view.columns = schema.columns;
867
1022
  view.totalRows = schema.totalRows;
868
1023
  view.loadedRows = 0;
@@ -870,8 +1025,7 @@ function BulkStreamProvider({ registry, children }) {
870
1025
  notify(view);
871
1026
  break;
872
1027
  }
873
- case FRAME_DATA:
874
- case FRAME_SEARCH_RESULT: {
1028
+ case FRAME_DATA: {
875
1029
  const rows = parseDataChunk(buf, store.columns);
876
1030
  store.appendChunk(rows);
877
1031
  view.loadedRows = store.getRowCount();
@@ -882,11 +1036,6 @@ function BulkStreamProvider({ registry, children }) {
882
1036
  view.streaming = false;
883
1037
  notify(view);
884
1038
  break;
885
- case FRAME_SEARCH_END:
886
- view.searchStreaming = false;
887
- view.streaming = false;
888
- notify(view);
889
- break;
890
1039
  case FRAME_DELTA_INS:
891
1040
  case FRAME_DELTA_UPD:
892
1041
  case FRAME_DELTA_DEL: {
@@ -910,6 +1059,12 @@ function BulkStreamProvider({ registry, children }) {
910
1059
  let cancelled = false;
911
1060
  let opened = false;
912
1061
  let ws = null;
1062
+ sendQueueRef.current = [];
1063
+ sidCounterRef.current = 1;
1064
+ viewsBySidRef.current.clear();
1065
+ viewsByKeyRef.current.clear();
1066
+ preloadedRef.current = false;
1067
+ setConnected(false);
913
1068
  setError(null);
914
1069
  (async () => {
915
1070
  let token = null;
@@ -959,7 +1114,7 @@ function BulkStreamProvider({ registry, children }) {
959
1114
  if (wsRef.current === ws) wsRef.current = null;
960
1115
  }
961
1116
  };
962
- }, []);
1117
+ }, [apiBaseUrl]);
963
1118
  useEffect3(() => {
964
1119
  if (!connected) return;
965
1120
  if (preloadedRef.current) return;
@@ -969,28 +1124,21 @@ function BulkStreamProvider({ registry, children }) {
969
1124
  for (const cart of keys) {
970
1125
  for (const ent of registry[cart] ?? []) {
971
1126
  const view = getView(cart, ent.name);
972
- sendCmd(encodeSchemaCmd(view.sid, view.cart, view.entity));
1127
+ ensureStreamed(view);
973
1128
  }
974
1129
  }
975
1130
  }, [connected, registry]);
976
1131
  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));
1132
+ view.store.search(query);
1133
+ view.totalRows = view.store.totalRows;
1134
+ view.loadedRows = view.store.getRowCount();
1135
+ notify(view);
1136
+ view.onRender?.();
988
1137
  };
989
1138
  const clearSearch = (view) => {
990
1139
  view.store.exitSearch();
991
1140
  view.totalRows = view.store.totalRows;
992
1141
  view.loadedRows = view.store.getRowCount();
993
- sendCmd(encodeClearSearchCmd(view.sid));
994
1142
  notify(view);
995
1143
  view.onRender?.();
996
1144
  };
@@ -1057,11 +1205,210 @@ function useBulkSubscription(cart, entity) {
1057
1205
  }
1058
1206
 
1059
1207
  // src/components/devtools/data-browser/data-table.tsx
1208
+ import React6 from "react";
1209
+
1210
+ // src/components/devtools/data-browser/value-popover.tsx
1060
1211
  import React5 from "react";
1061
- import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1212
+ import { createPortal } from "react-dom";
1213
+ import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1214
+ var WIDTH = 420;
1215
+ var INT_TYPES = /* @__PURE__ */ new Set([
1216
+ TYPE_U8,
1217
+ TYPE_U16,
1218
+ TYPE_U32,
1219
+ TYPE_U64,
1220
+ TYPE_S8,
1221
+ TYPE_S16,
1222
+ TYPE_S32,
1223
+ TYPE_S64
1224
+ ]);
1225
+ var FLOAT_TYPES = /* @__PURE__ */ new Set([TYPE_FLOAT32, TYPE_FLOAT64]);
1226
+ function position(rect) {
1227
+ const margin = 12;
1228
+ const left = Math.max(
1229
+ margin,
1230
+ Math.min(rect.left, window.innerWidth - WIDTH - margin)
1231
+ );
1232
+ const roomBelow = window.innerHeight - rect.bottom;
1233
+ const top = roomBelow >= 180 ? rect.bottom + 6 : Math.max(margin, rect.top - 188);
1234
+ return { left, top, width: WIDTH };
1235
+ }
1236
+ function coerce(draft, colType) {
1237
+ if (colType === TYPE_BOOL) return { value: draft === "true" };
1238
+ if (INT_TYPES.has(colType)) {
1239
+ const n = Number(draft);
1240
+ if (!Number.isInteger(n)) return { error: "Expected an integer" };
1241
+ return { value: n };
1242
+ }
1243
+ if (FLOAT_TYPES.has(colType)) {
1244
+ const n = Number(draft);
1245
+ if (!Number.isFinite(n)) return { error: "Expected a number" };
1246
+ return { value: n };
1247
+ }
1248
+ return { value: draft };
1249
+ }
1250
+ var ValuePopover = ({
1251
+ data,
1252
+ onClose
1253
+ }) => {
1254
+ const { apiBaseUrl } = useDevToolsConfig();
1255
+ const [copied, setCopied] = React5.useState(false);
1256
+ const [draft, setDraft] = React5.useState(data.value);
1257
+ const [saving, setSaving] = React5.useState(false);
1258
+ const [error, setError] = React5.useState(null);
1259
+ React5.useEffect(() => {
1260
+ setDraft(data.value);
1261
+ setError(null);
1262
+ }, [data.value, data.r, data.c]);
1263
+ React5.useEffect(() => {
1264
+ const onKey = (e) => {
1265
+ if (e.key === "Escape") onClose();
1266
+ };
1267
+ document.addEventListener("keydown", onKey);
1268
+ return () => document.removeEventListener("keydown", onKey);
1269
+ }, [onClose]);
1270
+ const copy = async () => {
1271
+ try {
1272
+ await navigator.clipboard.writeText(data.value);
1273
+ setCopied(true);
1274
+ } catch {
1275
+ }
1276
+ };
1277
+ const save = async () => {
1278
+ const result = coerce(draft, data.colType);
1279
+ if ("error" in result) {
1280
+ setError(result.error);
1281
+ return;
1282
+ }
1283
+ setSaving(true);
1284
+ setError(null);
1285
+ try {
1286
+ const r = await fetch(
1287
+ `${apiBaseUrl}/api/${data.cart}/${data.entity.toLowerCase()}/${encodeURIComponent(data.rowId)}`,
1288
+ {
1289
+ method: "PUT",
1290
+ credentials: "include",
1291
+ headers: { "content-type": "application/json" },
1292
+ body: JSON.stringify({ [data.colName]: result.value })
1293
+ }
1294
+ );
1295
+ if (!r.ok) {
1296
+ setError(await r.text() || `HTTP ${r.status}`);
1297
+ setSaving(false);
1298
+ return;
1299
+ }
1300
+ onClose();
1301
+ } catch (e) {
1302
+ setError(e instanceof Error ? e.message : String(e));
1303
+ setSaving(false);
1304
+ }
1305
+ };
1306
+ const isBool = data.colType === TYPE_BOOL;
1307
+ const isNumeric = INT_TYPES.has(data.colType) || FLOAT_TYPES.has(data.colType);
1308
+ return createPortal(
1309
+ /* @__PURE__ */ jsxs2(Fragment, { children: [
1310
+ /* @__PURE__ */ jsx5("div", { className: "fixed inset-0 z-[60]", onClick: onClose }),
1311
+ /* @__PURE__ */ jsxs2(
1312
+ "div",
1313
+ {
1314
+ className: cn(
1315
+ "nimbit-devtools dark fixed z-[61] rounded-md border border-border bg-popover p-3",
1316
+ "text-[12px] text-popover-foreground shadow-lg"
1317
+ ),
1318
+ style: position(data.rect),
1319
+ onClick: (e) => e.stopPropagation(),
1320
+ children: [
1321
+ /* @__PURE__ */ jsxs2("div", { className: "mb-2 flex items-center justify-between gap-3", children: [
1322
+ /* @__PURE__ */ jsxs2("div", { className: "min-w-0", children: [
1323
+ /* @__PURE__ */ jsx5("div", { className: "truncate font-medium text-foreground", children: data.colName }),
1324
+ /* @__PURE__ */ jsxs2("div", { className: "text-[11px] text-muted-foreground", children: [
1325
+ "Row ",
1326
+ data.r + 1
1327
+ ] })
1328
+ ] }),
1329
+ /* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-center gap-1.5", children: [
1330
+ /* @__PURE__ */ jsx5(
1331
+ "button",
1332
+ {
1333
+ type: "button",
1334
+ onClick: copy,
1335
+ className: cn(
1336
+ "rounded border border-border px-2 py-1 text-[11px] leading-none",
1337
+ "hover:bg-accent hover:text-accent-foreground"
1338
+ ),
1339
+ children: copied ? "Copied" : "Copy"
1340
+ }
1341
+ ),
1342
+ data.editable ? /* @__PURE__ */ jsx5(
1343
+ "button",
1344
+ {
1345
+ type: "button",
1346
+ onClick: save,
1347
+ disabled: saving,
1348
+ className: cn(
1349
+ "rounded border border-border bg-primary/90 px-2 py-1 text-[11px] leading-none text-primary-foreground",
1350
+ "hover:bg-primary disabled:opacity-50"
1351
+ ),
1352
+ children: saving ? "Saving\u2026" : "Save"
1353
+ }
1354
+ ) : null
1355
+ ] })
1356
+ ] }),
1357
+ data.editable ? /* @__PURE__ */ jsxs2(Fragment, { children: [
1358
+ isBool ? /* @__PURE__ */ jsxs2(
1359
+ "select",
1360
+ {
1361
+ value: draft,
1362
+ disabled: saving,
1363
+ onChange: (e) => setDraft(e.target.value),
1364
+ className: "w-full rounded-sm border border-border bg-background px-2 py-1 text-[12px] outline-none focus:border-accent-foreground/30",
1365
+ children: [
1366
+ /* @__PURE__ */ jsx5("option", { value: "true", children: "true" }),
1367
+ /* @__PURE__ */ jsx5("option", { value: "false", children: "false" })
1368
+ ]
1369
+ }
1370
+ ) : isNumeric ? /* @__PURE__ */ jsx5(
1371
+ "input",
1372
+ {
1373
+ type: "number",
1374
+ value: draft,
1375
+ disabled: saving,
1376
+ autoFocus: true,
1377
+ onChange: (e) => setDraft(e.target.value),
1378
+ onKeyDown: (e) => {
1379
+ if (e.key === "Enter") void save();
1380
+ },
1381
+ className: "w-full rounded-sm border border-border bg-background px-2 py-1 font-mono text-[12px] outline-none focus:border-accent-foreground/30"
1382
+ }
1383
+ ) : /* @__PURE__ */ jsx5(
1384
+ "textarea",
1385
+ {
1386
+ value: draft,
1387
+ disabled: saving,
1388
+ autoFocus: true,
1389
+ onChange: (e) => setDraft(e.target.value),
1390
+ className: "max-h-48 min-h-[3rem] w-full resize-y overflow-auto rounded-sm border border-border bg-background p-2 font-mono text-[11px] leading-5 outline-none focus:border-accent-foreground/30"
1391
+ }
1392
+ ),
1393
+ error ? /* @__PURE__ */ jsx5("div", { className: "mt-2 break-words text-[11px] text-red-400", children: error }) : null
1394
+ ] }) : /* @__PURE__ */ jsx5("div", { className: "max-h-48 overflow-auto rounded-sm bg-muted/40 p-2 font-mono text-[11px] leading-5 whitespace-pre-wrap break-words", children: data.value.length > 0 ? data.value : /* @__PURE__ */ jsx5("span", { className: "font-sans italic text-muted-foreground", children: "(empty)" }) })
1395
+ ]
1396
+ }
1397
+ )
1398
+ ] }),
1399
+ document.body
1400
+ );
1401
+ };
1402
+ var value_popover_default = ValuePopover;
1403
+
1404
+ // src/components/devtools/data-browser/data-table.tsx
1405
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1062
1406
  var ROW_H = 22;
1063
1407
  var OVERSCAN = 6;
1064
1408
  var MIN_COL_W = 80;
1409
+ var GUTTER_W = 32;
1410
+ var READONLY_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
1411
+ var READONLY_TYPES = /* @__PURE__ */ new Set([TYPE_DATETIME, TYPE_DOCUMENT]);
1065
1412
  function colWidth(name) {
1066
1413
  if (name === "id") return 220;
1067
1414
  if (name === "createdAt" || name === "updatedAt") return 180;
@@ -1071,16 +1418,18 @@ function colWidth(name) {
1071
1418
  }
1072
1419
  var DataTable = ({ cart, entity, searchFields, onSelectRow }) => {
1073
1420
  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(() => {
1421
+ const scrollerRef = React6.useRef(null);
1422
+ const [scrollTop, setScrollTop] = React6.useState(0);
1423
+ const [viewportH, setViewportH] = React6.useState(0);
1424
+ const [query, setQuery] = React6.useState("");
1425
+ const [selected, setSelected] = React6.useState(null);
1426
+ const [popover, setPopover] = React6.useState(null);
1427
+ const [tick, setTick] = React6.useState(0);
1428
+ React6.useEffect(() => {
1080
1429
  stream.setOnRender(() => setTick((n) => n + 1));
1081
1430
  return () => stream.setOnRender(null);
1082
1431
  }, [stream]);
1083
- React5.useEffect(() => {
1432
+ React6.useEffect(() => {
1084
1433
  const el = scrollerRef.current;
1085
1434
  if (!el) return;
1086
1435
  const ro = new ResizeObserver(() => setViewportH(el.clientHeight));
@@ -1110,10 +1459,42 @@ var DataTable = ({ cart, entity, searchFields, onSelectRow }) => {
1110
1459
  onSelectRow(out);
1111
1460
  };
1112
1461
  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(
1462
+ const totalColW = GUTTER_W + columns.reduce((s, c) => s + colWidth(c.name), 0);
1463
+ const idColIndex = columns.findIndex((c) => c.name === "id");
1464
+ const scrollRowIntoView = (r) => {
1465
+ const el = scrollerRef.current;
1466
+ if (!el) return;
1467
+ const top = r * ROW_H;
1468
+ if (top < el.scrollTop) el.scrollTop = top;
1469
+ else if (top + ROW_H > el.scrollTop + el.clientHeight)
1470
+ el.scrollTop = top + ROW_H - el.clientHeight;
1471
+ };
1472
+ const onKeyDown = (e) => {
1473
+ if (rows.length === 0 || columns.length === 0) return;
1474
+ if ((e.metaKey || e.ctrlKey) && (e.key === "c" || e.key === "C")) {
1475
+ if (!selected) return;
1476
+ const v = rows[selected.r]?.[selected.c] ?? "";
1477
+ void navigator.clipboard?.writeText(v);
1478
+ e.preventDefault();
1479
+ return;
1480
+ }
1481
+ const cur = selected ?? { r: start, c: 0 };
1482
+ let { r, c } = cur;
1483
+ if (e.key === "ArrowUp") r--;
1484
+ else if (e.key === "ArrowDown") r++;
1485
+ else if (e.key === "ArrowLeft") c--;
1486
+ else if (e.key === "ArrowRight") c++;
1487
+ else return;
1488
+ e.preventDefault();
1489
+ r = Math.max(0, Math.min(rows.length - 1, r));
1490
+ c = Math.max(0, Math.min(columns.length - 1, c));
1491
+ setSelected({ r, c });
1492
+ setPopover(null);
1493
+ scrollRowIntoView(r);
1494
+ };
1495
+ return /* @__PURE__ */ jsxs3("div", { className: "flex h-full flex-col bg-background", children: [
1496
+ /* @__PURE__ */ jsxs3("div", { className: "flex h-7 shrink-0 items-center gap-2 border-b border-border px-2", children: [
1497
+ /* @__PURE__ */ jsx6(
1117
1498
  "input",
1118
1499
  {
1119
1500
  type: "text",
@@ -1129,7 +1510,7 @@ var DataTable = ({ cart, entity, searchFields, onSelectRow }) => {
1129
1510
  )
1130
1511
  }
1131
1512
  ),
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: [
1513
+ /* @__PURE__ */ jsx6("div", { className: "shrink-0 text-[11px] text-muted-foreground tabular-nums", children: stream.error ? /* @__PURE__ */ jsx6("span", { className: "text-red-400", children: stream.error }) : !stream.connected ? /* @__PURE__ */ jsx6("span", { children: "connecting\u2026" }) : /* @__PURE__ */ jsxs3("span", { children: [
1133
1514
  stream.loadedRows.toLocaleString(),
1134
1515
  " / ",
1135
1516
  stream.totalRows.toLocaleString(),
@@ -1137,75 +1518,129 @@ var DataTable = ({ cart, entity, searchFields, onSelectRow }) => {
1137
1518
  stream.streaming ? " \xB7 streaming" : ""
1138
1519
  ] }) })
1139
1520
  ] }),
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(
1521
+ columns.length === 0 ? /* @__PURE__ */ jsx6("div", { className: "flex flex-1 items-center justify-center text-[12px] text-muted-foreground", children: stream.error ? stream.error : "Waiting for schema\u2026" }) : /* @__PURE__ */ jsx6(
1522
+ "div",
1523
+ {
1524
+ ref: scrollerRef,
1525
+ tabIndex: 0,
1526
+ onKeyDown,
1527
+ onScroll: (e) => {
1528
+ setScrollTop(e.target.scrollTop);
1529
+ if (popover) setPopover(null);
1530
+ },
1531
+ className: "group/table relative min-h-0 flex-1 overflow-auto font-mono outline-none",
1532
+ children: /* @__PURE__ */ jsxs3("div", { style: { width: totalColW }, children: [
1533
+ /* @__PURE__ */ jsxs3(
1150
1534
  "div",
1151
1535
  {
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) => {
1536
+ className: "sticky top-0 z-10 flex h-6 select-none border-b border-border bg-background text-[11px] uppercase tracking-wider text-muted-foreground",
1537
+ style: { width: totalColW },
1538
+ children: [
1539
+ /* @__PURE__ */ jsx6(
1540
+ "div",
1541
+ {
1542
+ style: { width: GUTTER_W },
1543
+ className: "shrink-0 border-r border-border"
1544
+ }
1545
+ ),
1546
+ columns.map((c) => /* @__PURE__ */ jsx6(
1547
+ "div",
1548
+ {
1549
+ style: { width: colWidth(c.name) },
1550
+ className: "flex items-center overflow-hidden border-r border-border px-2",
1551
+ children: /* @__PURE__ */ jsx6("span", { className: "truncate", children: c.name })
1552
+ },
1553
+ c.name
1554
+ ))
1555
+ ]
1556
+ }
1557
+ ),
1558
+ /* @__PURE__ */ jsx6("div", { style: { height: totalH, width: totalColW, position: "relative" }, children: rows.slice(start, end).map((row, i) => {
1170
1559
  const rowIdx = start + i;
1171
- return /* @__PURE__ */ jsx5(
1560
+ return /* @__PURE__ */ jsxs3(
1172
1561
  "div",
1173
1562
  {
1174
- onClick: () => handleRowClick(rowIdx),
1175
1563
  style: {
1176
1564
  top: rowIdx * ROW_H,
1177
1565
  height: ROW_H,
1178
1566
  width: totalColW
1179
1567
  },
1180
1568
  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"
1569
+ "absolute left-0 flex text-[12px] leading-none",
1570
+ rowIdx % 2 === 0 ? "bg-background" : "bg-muted/20"
1184
1571
  ),
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
- ))
1572
+ children: [
1573
+ /* @__PURE__ */ jsx6(
1574
+ "button",
1575
+ {
1576
+ type: "button",
1577
+ title: "Open row detail",
1578
+ onClick: () => handleRowClick(rowIdx),
1579
+ style: { width: GUTTER_W },
1580
+ className: cn(
1581
+ "flex shrink-0 items-center justify-center border-r border-border/60",
1582
+ "cursor-pointer text-[10px] tabular-nums text-muted-foreground/60",
1583
+ "hover:bg-accent hover:text-accent-foreground"
1584
+ ),
1585
+ children: rowIdx + 1
1586
+ }
1587
+ ),
1588
+ columns.map((c, ci) => {
1589
+ const isSel = selected?.r === rowIdx && selected?.c === ci;
1590
+ return /* @__PURE__ */ jsx6(
1591
+ "div",
1592
+ {
1593
+ "data-selected": isSel,
1594
+ onClick: (e) => {
1595
+ e.stopPropagation();
1596
+ setSelected({ r: rowIdx, c: ci });
1597
+ setPopover(null);
1598
+ },
1599
+ onDoubleClick: (e) => {
1600
+ e.stopPropagation();
1601
+ setSelected({ r: rowIdx, c: ci });
1602
+ const editable = idColIndex >= 0 && !READONLY_FIELDS.has(c.name) && !READONLY_TYPES.has(c.type);
1603
+ setPopover({
1604
+ r: rowIdx,
1605
+ c: ci,
1606
+ colName: c.name,
1607
+ value: row[ci] ?? "",
1608
+ rect: e.currentTarget.getBoundingClientRect(),
1609
+ cart,
1610
+ entity,
1611
+ rowId: idColIndex >= 0 ? row[idColIndex] ?? "" : "",
1612
+ colType: c.type,
1613
+ editable
1614
+ });
1615
+ },
1616
+ style: { width: colWidth(c.name) },
1617
+ className: cn(
1618
+ "flex cursor-pointer items-center overflow-hidden border-r border-border/60 px-2",
1619
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
1620
+ "data-[selected=true]:[box-shadow:inset_0_0_0_1px_#60a5fa]"
1621
+ ),
1622
+ title: row[ci],
1623
+ children: /* @__PURE__ */ jsx6("span", { className: "truncate", children: row[ci] })
1624
+ },
1625
+ c.name
1626
+ );
1627
+ })
1628
+ ]
1195
1629
  },
1196
1630
  rowIdx
1197
1631
  );
1198
1632
  }) })
1199
- }
1200
- )
1201
- ] }),
1202
- tick < 0 && /* @__PURE__ */ jsx5("span", {})
1633
+ ] })
1634
+ }
1635
+ ),
1636
+ popover ? /* @__PURE__ */ jsx6(value_popover_default, { data: popover, onClose: () => setPopover(null) }) : null,
1637
+ tick < 0 && /* @__PURE__ */ jsx6("span", {})
1203
1638
  ] });
1204
1639
  };
1205
1640
  var data_table_default = DataTable;
1206
1641
 
1207
1642
  // src/components/devtools/entity-graph/diagram-tab.tsx
1208
- import React6 from "react";
1643
+ import React7 from "react";
1209
1644
  import {
1210
1645
  Background,
1211
1646
  BackgroundVariant,
@@ -1216,18 +1651,17 @@ import {
1216
1651
  useNodesInitialized,
1217
1652
  useReactFlow
1218
1653
  } from "@xyflow/react";
1219
- import { Database as Database2, Eye, EyeOff, GitBranch, Rows3 } from "lucide-react";
1220
1654
 
1221
1655
  // src/components/devtools/entity-graph/entity-node.tsx
1222
1656
  import { Handle, Position } from "@xyflow/react";
1223
1657
  import { Database, FileText, KeyRound, Link2 } from "lucide-react";
1224
- import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1658
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1225
1659
  function EntityNode({ data, selected }) {
1226
1660
  const node = data;
1227
1661
  const highlight = node.highlight;
1228
1662
  const highlightedFields = new Set(highlight?.fields ?? []);
1229
1663
  const hiddenHandleClass = "!h-1 !w-1 !border-0 !bg-transparent !opacity-0";
1230
- return /* @__PURE__ */ jsxs3(
1664
+ return /* @__PURE__ */ jsxs4(
1231
1665
  "div",
1232
1666
  {
1233
1667
  className: [
@@ -1237,21 +1671,20 @@ function EntityNode({ data, selected }) {
1237
1671
  selected && !highlight?.focused ? "ring-2 ring-zinc-100/10" : ""
1238
1672
  ].join(" "),
1239
1673
  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 })
1674
+ /* @__PURE__ */ jsxs4("div", { className: "rounded-t-xl border-b border-zinc-700 bg-zinc-800 px-3 py-2", children: [
1675
+ /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 items-center gap-2", children: [
1676
+ /* @__PURE__ */ jsx7(Database, { className: "h-3.5 w-3.5 shrink-0 text-zinc-400" }),
1677
+ /* @__PURE__ */ jsx7("span", { className: "min-w-0 flex-1 truncate text-[13px] font-semibold leading-none text-zinc-50", children: node.entity })
1245
1678
  ] }),
1246
- /* @__PURE__ */ jsx6("div", { className: "mt-1 truncate text-[10px] text-zinc-500", children: node.cartridge })
1679
+ /* @__PURE__ */ jsx7("div", { className: "mt-1 truncate text-[10px] text-zinc-500", children: node.cartridge })
1247
1680
  ] }),
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) => {
1681
+ /* @__PURE__ */ jsx7("div", { className: "bg-zinc-900 py-1", children: node.fields.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "px-3 py-2 text-[10px] text-zinc-500", children: "No fields" }) : node.fields.map((field) => {
1249
1682
  const type = `${field.type}${field.array ? "[]" : ""}${field.optional ? "?" : ""}`;
1250
1683
  const isId = field.displayName.toLowerCase() === "id";
1251
1684
  const isRelation = field.kind === "relation";
1252
1685
  const isDocument = field.kind === "document";
1253
1686
  const fieldHighlighted = highlightedFields.has(field.displayName);
1254
- return /* @__PURE__ */ jsxs3(
1687
+ return /* @__PURE__ */ jsxs4(
1255
1688
  "div",
1256
1689
  {
1257
1690
  className: [
@@ -1259,8 +1692,8 @@ function EntityNode({ data, selected }) {
1259
1692
  fieldHighlighted ? "bg-blue-500/15 text-blue-200" : ""
1260
1693
  ].join(" "),
1261
1694
  children: [
1262
- isId ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1263
- /* @__PURE__ */ jsx6(
1695
+ isId ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1696
+ /* @__PURE__ */ jsx7(
1264
1697
  Handle,
1265
1698
  {
1266
1699
  id: `target-${field.displayName}-left`,
@@ -1270,7 +1703,7 @@ function EntityNode({ data, selected }) {
1270
1703
  style: { top: "50%" }
1271
1704
  }
1272
1705
  ),
1273
- /* @__PURE__ */ jsx6(
1706
+ /* @__PURE__ */ jsx7(
1274
1707
  Handle,
1275
1708
  {
1276
1709
  id: `target-${field.displayName}-right`,
@@ -1281,8 +1714,8 @@ function EntityNode({ data, selected }) {
1281
1714
  }
1282
1715
  )
1283
1716
  ] }) : null,
1284
- isRelation ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1285
- /* @__PURE__ */ jsx6(
1717
+ isRelation ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1718
+ /* @__PURE__ */ jsx7(
1286
1719
  Handle,
1287
1720
  {
1288
1721
  id: `source-${field.displayName}-left`,
@@ -1292,7 +1725,7 @@ function EntityNode({ data, selected }) {
1292
1725
  style: { top: "50%" }
1293
1726
  }
1294
1727
  ),
1295
- /* @__PURE__ */ jsx6(
1728
+ /* @__PURE__ */ jsx7(
1296
1729
  Handle,
1297
1730
  {
1298
1731
  id: `source-${field.displayName}-right`,
@@ -1303,24 +1736,24 @@ function EntityNode({ data, selected }) {
1303
1736
  }
1304
1737
  )
1305
1738
  ] }) : 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 })
1739
+ /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-1.5", children: [
1740
+ isId ? /* @__PURE__ */ jsx7(KeyRound, { className: "h-3 w-3 shrink-0 text-zinc-500" }) : null,
1741
+ isRelation ? /* @__PURE__ */ jsx7(Link2, { className: ["h-3 w-3 shrink-0", fieldHighlighted ? "text-blue-300" : "text-blue-400"].join(" ") }) : null,
1742
+ isDocument ? /* @__PURE__ */ jsx7(FileText, { className: "h-3 w-3 shrink-0 text-zinc-500" }) : null,
1743
+ /* @__PURE__ */ jsx7("span", { className: "truncate", children: field.displayName })
1311
1744
  ] }),
1312
- /* @__PURE__ */ jsx6("span", { className: "max-w-[96px] truncate text-right text-zinc-500", children: type })
1745
+ /* @__PURE__ */ jsx7("span", { className: "max-w-[96px] truncate text-right text-zinc-500", children: type })
1313
1746
  ]
1314
1747
  },
1315
1748
  `${field.name}:${field.displayName}`
1316
1749
  );
1317
1750
  }) }),
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: [
1751
+ /* @__PURE__ */ jsxs4("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: [
1752
+ /* @__PURE__ */ jsxs4("span", { className: "tabular-nums", children: [
1320
1753
  node.rowCount.toLocaleString(),
1321
1754
  " rows"
1322
1755
  ] }),
1323
- /* @__PURE__ */ jsxs3("span", { className: "tabular-nums", children: [
1756
+ /* @__PURE__ */ jsxs4("span", { className: "tabular-nums", children: [
1324
1757
  node.relationCount,
1325
1758
  " rel \xB7 ",
1326
1759
  node.scalarCount,
@@ -1469,7 +1902,7 @@ function layoutNodes(nodes, edges) {
1469
1902
  let maxY = 0;
1470
1903
  if (connected.length > 0) {
1471
1904
  const g = new dagre.graphlib.Graph();
1472
- g.setGraph({ rankdir: "LR", ranksep: 130, nodesep: 36, marginx: 40, marginy: 40 });
1905
+ g.setGraph({ rankdir: "LR", ranksep: 160, nodesep: 48, marginx: 40, marginy: 40 });
1473
1906
  g.setDefaultEdgeLabel(() => ({}));
1474
1907
  for (const node of connected) {
1475
1908
  g.setNode(node.id, { width: NODE_WIDTH, height: nodeHeight(node) });
@@ -1581,7 +2014,6 @@ function buildEntityGraphModel(cartridges) {
1581
2014
  }
1582
2015
  }
1583
2016
  recomputeNodeCounts(nodes);
1584
- layoutNodes(nodes, edges);
1585
2017
  return {
1586
2018
  nodes,
1587
2019
  edges,
@@ -1589,9 +2021,10 @@ function buildEntityGraphModel(cartridges) {
1589
2021
  };
1590
2022
  }
1591
2023
  function filterEntityGraphModel(model, visibleIds) {
1592
- const nodes = model.nodes.filter((node) => visibleIds.has(node.id));
2024
+ const nodes = model.nodes.filter((node) => visibleIds.has(node.id)).map((node) => ({ ...node, position: { ...node.position } }));
1593
2025
  const ids = new Set(nodes.map((node) => node.id));
1594
2026
  const edges = model.edges.filter((edge) => ids.has(edge.source) && ids.has(edge.target));
2027
+ layoutNodes(nodes, edges);
1595
2028
  return {
1596
2029
  nodes,
1597
2030
  edges,
@@ -1600,28 +2033,27 @@ function filterEntityGraphModel(model, visibleIds) {
1600
2033
  }
1601
2034
 
1602
2035
  // 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";
2036
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1606
2037
  var nodeTypes = { entity: EntityNode };
1607
2038
  var FitOnReady = ({ fitKey }) => {
1608
2039
  const initialized = useNodesInitialized();
1609
2040
  const { fitView } = useReactFlow();
1610
- React6.useEffect(() => {
2041
+ React7.useEffect(() => {
1611
2042
  if (initialized) fitView({ padding: 0.25, duration: 200 });
1612
2043
  }, [initialized, fitKey, fitView]);
1613
2044
  return null;
1614
2045
  };
1615
2046
  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(() => {
2047
+ const { apiBaseUrl } = useDevToolsConfig();
2048
+ const [contracts, setContracts] = React7.useState([]);
2049
+ const [loading, setLoading] = React7.useState(true);
2050
+ const [error, setError] = React7.useState(null);
2051
+ React7.useEffect(() => {
1620
2052
  const ac = new AbortController();
1621
2053
  let cancelled = false;
1622
2054
  (async () => {
1623
2055
  try {
1624
- const r = await fetch(`${API_BASE}/_console/contracts`, {
2056
+ const r = await fetch(`${apiBaseUrl}/_console/contracts`, {
1625
2057
  signal: ac.signal,
1626
2058
  credentials: "include"
1627
2059
  });
@@ -1640,7 +2072,7 @@ function useContracts() {
1640
2072
  cancelled = true;
1641
2073
  ac.abort();
1642
2074
  };
1643
- }, []);
2075
+ }, [apiBaseUrl]);
1644
2076
  return { contracts, loading, error };
1645
2077
  }
1646
2078
  function registryFromContracts(contracts) {
@@ -1656,58 +2088,40 @@ function registryFromContracts(contracts) {
1656
2088
  }
1657
2089
  return reg;
1658
2090
  }
1659
- var DiagramView = () => {
2091
+ var DiagramView = ({ visibleIds, onSelectNode }) => {
1660
2092
  const { contracts, loading, error } = useContracts();
1661
- const graph = React6.useMemo(
2093
+ const graph = React7.useMemo(
1662
2094
  () => buildEntityGraphModel(cartsFromContracts(contracts)),
1663
2095
  [contracts]
1664
2096
  );
1665
- const registry = React6.useMemo(() => registryFromContracts(contracts), [contracts]);
2097
+ const registry = React7.useMemo(() => registryFromContracts(contracts), [contracts]);
1666
2098
  if (graph.nodes.length === 0) {
1667
2099
  let msg = "No installed cartridges with entities.";
1668
2100
  if (loading) msg = "Loading entity graph\u2026";
1669
2101
  else if (error) msg = `Failed to load contracts: ${error}`;
1670
- return /* @__PURE__ */ jsx7("div", { className: "p-3 text-[12px] text-muted-foreground", children: msg });
2102
+ return /* @__PURE__ */ jsx8("div", { className: "p-3 text-[12px] text-muted-foreground", children: msg });
1671
2103
  }
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();
2104
+ return /* @__PURE__ */ jsx8(
2105
+ DiagramInner,
2106
+ {
2107
+ graph,
2108
+ registry,
2109
+ visibleIds,
2110
+ onSelectNode
1687
2111
  }
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
2112
  );
1703
- const visibleGraph = React6.useMemo(
2113
+ };
2114
+ var DiagramInner = ({ graph, registry, visibleIds, onSelectNode }) => {
2115
+ const rowCounts = useBulkRowCounts(registry);
2116
+ const [focusedNodeId, setFocusedNodeId] = React7.useState(null);
2117
+ const visibleGraph = React7.useMemo(
1704
2118
  () => filterEntityGraphModel(graph, visibleIds),
1705
2119
  [graph, visibleIds]
1706
2120
  );
1707
- React6.useEffect(() => {
2121
+ React7.useEffect(() => {
1708
2122
  if (focusedNodeId && !visibleIds.has(focusedNodeId)) setFocusedNodeId(null);
1709
2123
  }, [focusedNodeId, visibleIds]);
1710
- const focusedConnections = React6.useMemo(() => {
2124
+ const focusedConnections = React7.useMemo(() => {
1711
2125
  if (!focusedNodeId) {
1712
2126
  return {
1713
2127
  connectedIds: /* @__PURE__ */ new Set(),
@@ -1733,11 +2147,7 @@ var DiagramInner = ({
1733
2147
  }
1734
2148
  return { connectedIds, edgeIds, fieldsByNode };
1735
2149
  }, [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(
2150
+ const nodes = React7.useMemo(
1741
2151
  () => visibleGraph.nodes.map((node) => ({
1742
2152
  id: node.id,
1743
2153
  type: "entity",
@@ -1755,7 +2165,7 @@ var DiagramInner = ({
1755
2165
  })),
1756
2166
  [focusedConnections, focusedNodeId, visibleGraph.nodes, rowCounts]
1757
2167
  );
1758
- const edges = React6.useMemo(() => {
2168
+ const edges = React7.useMemo(() => {
1759
2169
  const nodeById = new Map(visibleGraph.nodes.map((node) => [node.id, node]));
1760
2170
  const laneCounts = /* @__PURE__ */ new Map();
1761
2171
  return visibleGraph.edges.map((edge) => {
@@ -1787,95 +2197,54 @@ var DiagramInner = ({
1787
2197
  };
1788
2198
  });
1789
2199
  }, [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
- ] });
2200
+ const onNodeClick = React7.useCallback(
2201
+ (_, node) => {
2202
+ const data = node.data;
2203
+ const nodeId = entityGraphId(data.cartridge, data.entity);
2204
+ setFocusedNodeId((prev) => prev === nodeId ? null : nodeId);
2205
+ onSelectNode?.(data.cartridge, data.entity);
2206
+ },
2207
+ [onSelectNode]
2208
+ );
2209
+ const onNodeContextMenu = React7.useCallback(
2210
+ (e, node) => {
2211
+ e.preventDefault();
2212
+ const data = node.data;
2213
+ setFocusedNodeId(entityGraphId(data.cartridge, data.entity));
2214
+ onSelectNode?.(data.cartridge, data.entity);
2215
+ },
2216
+ [onSelectNode]
2217
+ );
2218
+ const clearFocusedNode = React7.useCallback(() => setFocusedNodeId(null), []);
2219
+ return /* @__PURE__ */ jsx8("div", { className: "flex h-full min-h-0 w-full flex-col", children: /* @__PURE__ */ jsx8("div", { className: "relative min-h-0 flex-1 bg-zinc-950", children: nodes.length === 0 ? /* @__PURE__ */ jsx8("div", { className: "flex h-full items-center justify-center text-[12px] text-zinc-500", children: "No entities are visible." }) : /* @__PURE__ */ jsx8(ReactFlowProvider, { children: /* @__PURE__ */ jsxs5(
2220
+ ReactFlow,
2221
+ {
2222
+ colorMode: "dark",
2223
+ nodes,
2224
+ edges,
2225
+ nodeTypes,
2226
+ fitView: true,
2227
+ fitViewOptions: { padding: 0.2 },
2228
+ minZoom: 0.1,
2229
+ maxZoom: 1.6,
2230
+ nodesDraggable: false,
2231
+ nodesConnectable: false,
2232
+ elementsSelectable: true,
2233
+ panOnDrag: true,
2234
+ zoomOnScroll: true,
2235
+ zoomOnPinch: true,
2236
+ selectionOnDrag: false,
2237
+ onNodeClick,
2238
+ onNodeContextMenu,
2239
+ onPaneClick: clearFocusedNode,
2240
+ proOptions: { hideAttribution: true },
2241
+ children: [
2242
+ /* @__PURE__ */ jsx8(FitOnReady, { fitKey: [...visibleIds].sort().join("|") }),
2243
+ /* @__PURE__ */ jsx8(Background, { variant: BackgroundVariant.Dots, gap: 16, size: 1, color: "#3f3f46" }),
2244
+ /* @__PURE__ */ jsx8(Controls, { showInteractive: false })
2245
+ ]
2246
+ }
2247
+ ) }) }) });
1879
2248
  };
1880
2249
 
1881
2250
  // src/components/ui/sheet.tsx
@@ -1884,7 +2253,7 @@ import { Dialog as SheetPrimitive } from "radix-ui";
1884
2253
  // src/components/ui/button.tsx
1885
2254
  import { cva } from "class-variance-authority";
1886
2255
  import { Slot } from "radix-ui";
1887
- import { jsx as jsx8 } from "react/jsx-runtime";
2256
+ import { jsx as jsx9 } from "react/jsx-runtime";
1888
2257
  var buttonVariants = cva(
1889
2258
  "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
2259
  {
@@ -1922,7 +2291,7 @@ function Button({
1922
2291
  ...props
1923
2292
  }) {
1924
2293
  const Comp = asChild ? Slot.Root : "button";
1925
- return /* @__PURE__ */ jsx8(
2294
+ return /* @__PURE__ */ jsx9(
1926
2295
  Comp,
1927
2296
  {
1928
2297
  "data-slot": "button",
@@ -1936,20 +2305,20 @@ function Button({
1936
2305
 
1937
2306
  // src/components/ui/sheet.tsx
1938
2307
  import { XIcon } from "lucide-react";
1939
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2308
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1940
2309
  function Sheet({ ...props }) {
1941
- return /* @__PURE__ */ jsx9(SheetPrimitive.Root, { "data-slot": "sheet", ...props });
2310
+ return /* @__PURE__ */ jsx10(SheetPrimitive.Root, { "data-slot": "sheet", ...props });
1942
2311
  }
1943
2312
  function SheetPortal({
1944
2313
  ...props
1945
2314
  }) {
1946
- return /* @__PURE__ */ jsx9(SheetPrimitive.Portal, { "data-slot": "sheet-portal", ...props });
2315
+ return /* @__PURE__ */ jsx10(SheetPrimitive.Portal, { "data-slot": "sheet-portal", ...props });
1947
2316
  }
1948
2317
  function SheetOverlay({
1949
2318
  className,
1950
2319
  ...props
1951
2320
  }) {
1952
- return /* @__PURE__ */ jsx9(
2321
+ return /* @__PURE__ */ jsx10(
1953
2322
  SheetPrimitive.Overlay,
1954
2323
  {
1955
2324
  "data-slot": "sheet-overlay",
@@ -1968,9 +2337,9 @@ function SheetContent({
1968
2337
  showCloseButton = true,
1969
2338
  ...props
1970
2339
  }) {
1971
- return /* @__PURE__ */ jsxs5(SheetPortal, { children: [
1972
- /* @__PURE__ */ jsx9(SheetOverlay, {}),
1973
- /* @__PURE__ */ jsxs5(
2340
+ return /* @__PURE__ */ jsxs6(SheetPortal, { children: [
2341
+ /* @__PURE__ */ jsx10(SheetOverlay, {}),
2342
+ /* @__PURE__ */ jsxs6(
1974
2343
  SheetPrimitive.Content,
1975
2344
  {
1976
2345
  "data-slot": "sheet-content",
@@ -1982,18 +2351,18 @@ function SheetContent({
1982
2351
  ...props,
1983
2352
  children: [
1984
2353
  children,
1985
- showCloseButton && /* @__PURE__ */ jsx9(SheetPrimitive.Close, { "data-slot": "sheet-close", asChild: true, children: /* @__PURE__ */ jsxs5(
2354
+ showCloseButton && /* @__PURE__ */ jsx10(SheetPrimitive.Close, { "data-slot": "sheet-close", asChild: true, children: /* @__PURE__ */ jsxs6(
1986
2355
  Button,
1987
2356
  {
1988
2357
  variant: "ghost",
1989
2358
  className: "absolute top-3 right-3",
1990
2359
  size: "icon-sm",
1991
2360
  children: [
1992
- /* @__PURE__ */ jsx9(
2361
+ /* @__PURE__ */ jsx10(
1993
2362
  XIcon,
1994
2363
  {}
1995
2364
  ),
1996
- /* @__PURE__ */ jsx9("span", { className: "sr-only", children: "Close" })
2365
+ /* @__PURE__ */ jsx10("span", { className: "sr-only", children: "Close" })
1997
2366
  ]
1998
2367
  }
1999
2368
  ) })
@@ -2003,7 +2372,7 @@ function SheetContent({
2003
2372
  ] });
2004
2373
  }
2005
2374
  function SheetHeader({ className, ...props }) {
2006
- return /* @__PURE__ */ jsx9(
2375
+ return /* @__PURE__ */ jsx10(
2007
2376
  "div",
2008
2377
  {
2009
2378
  "data-slot": "sheet-header",
@@ -2016,7 +2385,7 @@ function SheetTitle({
2016
2385
  className,
2017
2386
  ...props
2018
2387
  }) {
2019
- return /* @__PURE__ */ jsx9(
2388
+ return /* @__PURE__ */ jsx10(
2020
2389
  SheetPrimitive.Title,
2021
2390
  {
2022
2391
  "data-slot": "sheet-title",
@@ -2032,7 +2401,7 @@ function SheetDescription({
2032
2401
  className,
2033
2402
  ...props
2034
2403
  }) {
2035
- return /* @__PURE__ */ jsx9(
2404
+ return /* @__PURE__ */ jsx10(
2036
2405
  SheetPrimitive.Description,
2037
2406
  {
2038
2407
  "data-slot": "sheet-description",
@@ -2043,16 +2412,63 @@ function SheetDescription({
2043
2412
  }
2044
2413
 
2045
2414
  // src/components/devtools/data-tab.tsx
2046
- import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2415
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2047
2416
  var DataTab = () => {
2048
- const { dataCart, setDataCart, dataEntity, setDataEntity } = useDevTools();
2049
- const [detail, setDetail] = React7.useState(null);
2050
- const [view, setView] = React7.useState("table");
2417
+ const {
2418
+ dataCart,
2419
+ setDataCart,
2420
+ dataEntity,
2421
+ setDataEntity,
2422
+ view,
2423
+ setView,
2424
+ graphCarts,
2425
+ setGraphCarts,
2426
+ hiddenEntities,
2427
+ setHiddenEntities,
2428
+ graphLeftW,
2429
+ setGraphLeftW,
2430
+ graphRightW,
2431
+ setGraphRightW
2432
+ } = useDevTools();
2433
+ const leftRef = React8.useRef(null);
2434
+ const rightRef = React8.useRef(null);
2435
+ const [detail, setDetail] = React8.useState(null);
2436
+ const [graphSel, setGraphSel] = React8.useState(null);
2051
2437
  const { registry, carts, loading, error } = useLiveBulkRegistry();
2438
+ const selectedCarts = graphCarts ?? new Set(carts);
2439
+ const toggleGraphCart = (cart) => {
2440
+ setGraphCarts((prev) => {
2441
+ const next = new Set(prev ?? carts);
2442
+ if (next.has(cart)) next.delete(cart);
2443
+ else next.add(cart);
2444
+ return next;
2445
+ });
2446
+ };
2447
+ const toggleEntity = (id) => {
2448
+ setHiddenEntities((prev) => {
2449
+ const next = new Set(prev);
2450
+ if (next.has(id)) next.delete(id);
2451
+ else next.add(id);
2452
+ return next;
2453
+ });
2454
+ };
2455
+ const visibleIds = React8.useMemo(() => {
2456
+ const ids = /* @__PURE__ */ new Set();
2457
+ for (const cart of carts) {
2458
+ if (!selectedCarts.has(cart)) continue;
2459
+ for (const e of registry[cart] ?? [])
2460
+ if (!hiddenEntities.has(`${cart}:${e.name}`)) ids.add(`${cart}:${e.name}`);
2461
+ }
2462
+ return ids;
2463
+ }, [carts, registry, graphCarts, hiddenEntities]);
2464
+ React8.useEffect(() => {
2465
+ if (graphSel && !visibleIds.has(`${graphSel.cart}:${graphSel.entity}`))
2466
+ setGraphSel(null);
2467
+ }, [graphSel, visibleIds]);
2052
2468
  const activeCart = dataCart && registry[dataCart] ? dataCart : carts[0] ?? null;
2053
2469
  const entities = activeCart ? registry[activeCart] ?? [] : [];
2054
2470
  const activeEntity = entities.find((e) => e.name === dataEntity) ?? entities[0] ?? null;
2055
- React7.useEffect(() => {
2471
+ React8.useEffect(() => {
2056
2472
  if (activeCart && activeCart !== dataCart) setDataCart(activeCart);
2057
2473
  if (activeEntity && activeEntity.name !== dataEntity) setDataEntity(activeEntity.name);
2058
2474
  }, [activeCart, activeEntity, dataCart, dataEntity, setDataCart, setDataEntity]);
@@ -2060,60 +2476,100 @@ var DataTab = () => {
2060
2476
  let msg = "No installed cartridges with entities.";
2061
2477
  if (loading) msg = "Loading cartridges\u2026";
2062
2478
  else if (error) msg = `Failed to load cartridges: ${error}`;
2063
- return /* @__PURE__ */ jsx10("div", { className: "p-3 text-[12px] text-muted-foreground", children: msg });
2479
+ return /* @__PURE__ */ jsx11("div", { className: "p-3 text-[12px] text-muted-foreground", children: msg });
2064
2480
  }
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" }) })
2481
+ return /* @__PURE__ */ jsx11(BulkStreamProvider, { registry, children: /* @__PURE__ */ jsxs7("div", { className: "flex h-full flex-col", children: [
2482
+ /* @__PURE__ */ jsxs7("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: [
2483
+ /* @__PURE__ */ jsxs7("div", { className: "flex shrink-0 items-center gap-0.5 pr-1", children: [
2484
+ /* @__PURE__ */ jsx11(ViewBtn, { active: view === "table", onClick: () => setView("table"), label: "Table", children: /* @__PURE__ */ jsx11(Table2, { className: "size-3" }) }),
2485
+ /* @__PURE__ */ jsx11(ViewBtn, { active: view === "diagram", onClick: () => setView("diagram"), label: "Graph", children: /* @__PURE__ */ jsx11(Network, { className: "size-3" }) })
2070
2486
  ] }),
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(
2487
+ /* @__PURE__ */ jsx11("div", { className: "h-4 w-px shrink-0 bg-border" }),
2488
+ carts.map((cart) => {
2489
+ const isActive = view === "table" ? cart === activeCart : selectedCarts.has(cart);
2490
+ return /* @__PURE__ */ jsx11(
2101
2491
  "button",
2102
2492
  {
2103
2493
  type: "button",
2104
2494
  "data-active": isActive,
2105
- onClick: () => setDataEntity(e.name),
2495
+ onClick: () => {
2496
+ if (view === "table") {
2497
+ setDataCart(cart);
2498
+ const first = registry[cart]?.[0];
2499
+ setDataEntity(first ? first.name : null);
2500
+ } else {
2501
+ toggleGraphCart(cart);
2502
+ }
2503
+ },
2106
2504
  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"
2505
+ "h-5 shrink-0 rounded-full px-2.5 text-[11px] leading-none outline-none transition-colors",
2506
+ "border border-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground",
2507
+ "data-[active=true]:border-border data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
2110
2508
  ),
2111
- children: e.name
2509
+ children: cart
2112
2510
  },
2113
- e.name
2511
+ cart
2114
2512
  );
2115
- }) }),
2116
- /* @__PURE__ */ jsx10("div", { className: "min-h-0 flex-1", children: activeEntity ? /* @__PURE__ */ jsx10(
2513
+ })
2514
+ ] }),
2515
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-h-0 min-w-0 flex-1", children: [
2516
+ /* @__PURE__ */ jsxs7(
2517
+ "div",
2518
+ {
2519
+ ref: leftRef,
2520
+ style: { width: graphLeftW },
2521
+ className: "relative shrink-0 border-r border-border bg-muted/10",
2522
+ children: [
2523
+ /* @__PURE__ */ jsx11(ResizeHandle, { edge: "right", targetRef: leftRef, min: 120, max: 480, onCommit: setGraphLeftW }),
2524
+ /* @__PURE__ */ jsx11("div", { className: "flex h-full flex-col overflow-y-auto py-1", children: view === "table" ? entities.length === 0 ? /* @__PURE__ */ jsx11("div", { className: "px-2 py-1 text-[11px] text-muted-foreground", children: "No entities" }) : entities.map((e) => {
2525
+ const isActive = e.name === activeEntity?.name;
2526
+ return /* @__PURE__ */ jsx11(
2527
+ "button",
2528
+ {
2529
+ type: "button",
2530
+ "data-active": isActive,
2531
+ onClick: () => setDataEntity(e.name),
2532
+ className: cn(
2533
+ "h-6 shrink-0 text-left px-2 text-[12px] leading-none outline-none transition-colors",
2534
+ "text-foreground hover:bg-accent hover:text-accent-foreground",
2535
+ "data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
2536
+ ),
2537
+ children: e.name
2538
+ },
2539
+ e.name
2540
+ );
2541
+ }) : carts.filter((c) => selectedCarts.has(c)).length === 0 ? /* @__PURE__ */ jsx11("div", { className: "px-2 py-1 text-[11px] text-muted-foreground", children: "No cartridges selected" }) : carts.filter((c) => selectedCarts.has(c)).map((cart) => /* @__PURE__ */ jsxs7("div", { className: "mb-1", children: [
2542
+ /* @__PURE__ */ jsx11("div", { className: "px-2 py-0.5 text-[10px] uppercase tracking-wider text-muted-foreground/60", children: cart }),
2543
+ (registry[cart] ?? []).map((e) => {
2544
+ const id = `${cart}:${e.name}`;
2545
+ const on = !hiddenEntities.has(id);
2546
+ return /* @__PURE__ */ jsx11(
2547
+ "button",
2548
+ {
2549
+ type: "button",
2550
+ "data-active": on,
2551
+ onClick: () => toggleEntity(id),
2552
+ title: on ? `Hide ${e.name}` : `Show ${e.name}`,
2553
+ className: cn(
2554
+ "h-6 w-full shrink-0 text-left px-2 text-[12px] leading-none outline-none transition-colors hover:bg-accent hover:text-accent-foreground",
2555
+ on ? "text-foreground" : "text-muted-foreground/40 line-through"
2556
+ ),
2557
+ children: e.name
2558
+ },
2559
+ id
2560
+ );
2561
+ })
2562
+ ] }, cart)) })
2563
+ ]
2564
+ }
2565
+ ),
2566
+ /* @__PURE__ */ jsx11("div", { className: "min-h-0 min-w-0 flex-1", children: view === "diagram" ? /* @__PURE__ */ jsx11(
2567
+ DiagramView,
2568
+ {
2569
+ visibleIds,
2570
+ onSelectNode: (cart, entity) => setGraphSel({ cart, entity })
2571
+ }
2572
+ ) : activeEntity ? /* @__PURE__ */ jsx11(
2117
2573
  data_table_default,
2118
2574
  {
2119
2575
  cart: activeCart,
@@ -2122,26 +2578,125 @@ var DataTab = () => {
2122
2578
  onSelectRow: (row) => setDetail(row)
2123
2579
  },
2124
2580
  `${activeCart}:${activeEntity.name}`
2125
- ) : /* @__PURE__ */ jsx10("div", { className: "p-3 text-[12px] text-muted-foreground", children: "Pick an entity." }) })
2581
+ ) : /* @__PURE__ */ jsx11("div", { className: "p-3 text-[12px] text-muted-foreground", children: "Pick an entity." }) }),
2582
+ view === "diagram" && graphSel ? /* @__PURE__ */ jsxs7(
2583
+ "div",
2584
+ {
2585
+ ref: rightRef,
2586
+ style: { width: graphRightW },
2587
+ className: "relative flex shrink-0 flex-col border-l border-border bg-background",
2588
+ children: [
2589
+ /* @__PURE__ */ jsx11(ResizeHandle, { edge: "left", targetRef: rightRef, min: 280, max: 900, onCommit: setGraphRightW }),
2590
+ /* @__PURE__ */ jsxs7("div", { className: "flex h-7 shrink-0 items-center gap-1 border-b border-border bg-muted/10 px-2 select-none", children: [
2591
+ /* @__PURE__ */ jsxs7("span", { className: "min-w-0 flex-1 truncate text-[12px]", children: [
2592
+ /* @__PURE__ */ jsxs7("span", { className: "text-muted-foreground", children: [
2593
+ graphSel.cart,
2594
+ "."
2595
+ ] }),
2596
+ graphSel.entity
2597
+ ] }),
2598
+ /* @__PURE__ */ jsx11(
2599
+ "button",
2600
+ {
2601
+ type: "button",
2602
+ "aria-label": "Close",
2603
+ title: "Close",
2604
+ onClick: () => setGraphSel(null),
2605
+ className: "grid size-5 shrink-0 place-items-center rounded-md text-foreground outline-none hover:bg-accent hover:text-accent-foreground",
2606
+ children: /* @__PURE__ */ jsx11(X, { className: "size-3.5" })
2607
+ }
2608
+ )
2609
+ ] }),
2610
+ /* @__PURE__ */ jsx11("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx11(
2611
+ data_table_default,
2612
+ {
2613
+ cart: graphSel.cart,
2614
+ entity: graphSel.entity,
2615
+ searchFields: (registry[graphSel.cart] ?? []).find((e) => e.name === graphSel.entity)?.searchFields ?? [],
2616
+ onSelectRow: (row) => setDetail(row)
2617
+ },
2618
+ `${graphSel.cart}:${graphSel.entity}`
2619
+ ) })
2620
+ ]
2621
+ }
2622
+ ) : null
2126
2623
  ] }),
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: [
2624
+ /* @__PURE__ */ jsx11(Sheet, { open: !!detail, onOpenChange: (o) => !o && setDetail(null), children: /* @__PURE__ */ jsxs7(SheetContent, { side: "right", className: "w-[480px] sm:max-w-[480px]", children: [
2625
+ /* @__PURE__ */ jsxs7(SheetHeader, { children: [
2626
+ /* @__PURE__ */ jsxs7(SheetTitle, { className: "text-[13px]", children: [
2130
2627
  activeCart,
2131
2628
  ".",
2132
2629
  activeEntity?.name,
2133
- detail?.id ? /* @__PURE__ */ jsx10("span", { className: "ml-2 font-mono text-[11px] text-muted-foreground", children: detail.id }) : null
2630
+ detail?.id ? /* @__PURE__ */ jsx11("span", { className: "ml-2 font-mono text-[11px] text-muted-foreground", children: detail.id }) : null
2134
2631
  ] }),
2135
- /* @__PURE__ */ jsx10(SheetDescription, { className: "text-[11px]", children: "Row detail" })
2632
+ /* @__PURE__ */ jsx11(SheetDescription, { className: "text-[11px]", children: "Row detail" })
2136
2633
  ] }),
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" }) })
2634
+ /* @__PURE__ */ jsx11("div", { className: "min-h-0 flex-1 overflow-auto px-4 pb-4", children: detail ? /* @__PURE__ */ jsx11("table", { className: "w-full text-[12px]", children: /* @__PURE__ */ jsx11("tbody", { children: Object.entries(detail).map(([k, v]) => /* @__PURE__ */ jsxs7("tr", { className: "border-b border-border/40 align-top", children: [
2635
+ /* @__PURE__ */ jsx11("td", { className: "w-32 py-1 pr-2 font-mono text-[11px] text-muted-foreground", children: k }),
2636
+ /* @__PURE__ */ jsx11("td", { className: "break-all py-1 font-mono", children: v || /* @__PURE__ */ jsx11("span", { className: "text-muted-foreground/60", children: "\u2205" }) })
2140
2637
  ] }, k)) }) }) : null })
2141
2638
  ] }) })
2142
2639
  ] }) });
2143
2640
  };
2144
- var ViewBtn = ({ active, onClick, label, children }) => /* @__PURE__ */ jsxs6(
2641
+ var ResizeHandle = ({ edge, targetRef, min, max, onCommit }) => {
2642
+ const onPointerDown = (e) => {
2643
+ e.preventDefault();
2644
+ const el = targetRef.current;
2645
+ if (!el) return;
2646
+ const handle = e.currentTarget;
2647
+ handle.setPointerCapture(e.pointerId);
2648
+ const startX = e.clientX;
2649
+ const startW = el.offsetWidth;
2650
+ let last = startW;
2651
+ let raf = 0;
2652
+ let pending = null;
2653
+ const flush = () => {
2654
+ raf = 0;
2655
+ if (pending == null) return;
2656
+ last = pending;
2657
+ el.style.width = `${pending}px`;
2658
+ pending = null;
2659
+ };
2660
+ const onMove = (ev) => {
2661
+ ev.preventDefault();
2662
+ const dx = ev.clientX - startX;
2663
+ const delta = edge === "left" ? -dx : dx;
2664
+ pending = Math.max(min, Math.min(max, startW + delta));
2665
+ if (!raf) raf = requestAnimationFrame(flush);
2666
+ };
2667
+ const onUp = () => {
2668
+ if (raf) cancelAnimationFrame(raf);
2669
+ flush();
2670
+ onCommit(last);
2671
+ handle.removeEventListener("pointermove", onMove);
2672
+ handle.removeEventListener("pointerup", onUp);
2673
+ handle.removeEventListener("pointercancel", onUp);
2674
+ try {
2675
+ handle.releasePointerCapture(e.pointerId);
2676
+ } catch {
2677
+ }
2678
+ document.body.style.userSelect = "";
2679
+ document.body.style.cursor = "";
2680
+ };
2681
+ document.body.style.userSelect = "none";
2682
+ document.body.style.cursor = "ew-resize";
2683
+ handle.addEventListener("pointermove", onMove);
2684
+ handle.addEventListener("pointerup", onUp);
2685
+ handle.addEventListener("pointercancel", onUp);
2686
+ };
2687
+ return /* @__PURE__ */ jsx11(
2688
+ "div",
2689
+ {
2690
+ onPointerDown,
2691
+ style: { touchAction: "none" },
2692
+ className: cn(
2693
+ "absolute top-0 bottom-0 z-20 w-1.5 cursor-ew-resize bg-transparent hover:bg-accent/40",
2694
+ edge === "left" ? "-left-px" : "-right-px"
2695
+ )
2696
+ }
2697
+ );
2698
+ };
2699
+ var ViewBtn = ({ active, onClick, label, children }) => /* @__PURE__ */ jsxs7(
2145
2700
  "button",
2146
2701
  {
2147
2702
  type: "button",
@@ -2162,22 +2717,1112 @@ var ViewBtn = ({ active, onClick, label, children }) => /* @__PURE__ */ jsxs6(
2162
2717
  var data_tab_default = DataTab;
2163
2718
 
2164
2719
  // src/components/devtools/settings-tab.tsx
2165
- import { Fragment as Fragment3, jsx as jsx11 } from "react/jsx-runtime";
2720
+ import { Fragment as Fragment3, jsx as jsx12 } from "react/jsx-runtime";
2166
2721
  var SettingsTab = () => {
2167
- return /* @__PURE__ */ jsx11(Fragment3, {});
2722
+ return /* @__PURE__ */ jsx12(Fragment3, {});
2168
2723
  };
2169
2724
  var settings_tab_default = SettingsTab;
2170
2725
 
2726
+ // src/components/devtools/sources/sources-tab.tsx
2727
+ import React11 from "react";
2728
+ import { Play as Play2, Save } from "lucide-react";
2729
+
2730
+ // src/components/devtools/sources/use-dev-files.ts
2731
+ import { useCallback, useEffect as useEffect4, useState as useState3 } from "react";
2732
+ function useDevMode() {
2733
+ const { apiBaseUrl } = useDevToolsConfig();
2734
+ const [status, setStatus] = useState3(null);
2735
+ useEffect4(() => {
2736
+ let cancelled = false;
2737
+ fetch(`${apiBaseUrl}/_console/dev/status`, { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((s) => {
2738
+ if (!cancelled) setStatus(s && s.dev ? s : null);
2739
+ }).catch(() => {
2740
+ if (!cancelled) setStatus(null);
2741
+ });
2742
+ return () => {
2743
+ cancelled = true;
2744
+ };
2745
+ }, [apiBaseUrl]);
2746
+ return status;
2747
+ }
2748
+ async function jsonOrThrow(r) {
2749
+ const body = await r.json().catch(() => ({}));
2750
+ if (!r.ok) throw new Error(body?.error ?? `HTTP ${r.status}`);
2751
+ return body;
2752
+ }
2753
+ function useDevFilesApi() {
2754
+ const { apiBaseUrl } = useDevToolsConfig();
2755
+ const listCarts = useCallback(
2756
+ async () => (await jsonOrThrow(await fetch(`${apiBaseUrl}/_console/dev/carts`, { credentials: "include" }))).carts ?? [],
2757
+ [apiBaseUrl]
2758
+ );
2759
+ const readFile = useCallback(
2760
+ async (cart, path) => jsonOrThrow(
2761
+ await fetch(
2762
+ `${apiBaseUrl}/_console/dev/file?cart=${encodeURIComponent(cart)}&path=${encodeURIComponent(path)}`,
2763
+ { credentials: "include" }
2764
+ )
2765
+ ),
2766
+ [apiBaseUrl]
2767
+ );
2768
+ const writeFile = useCallback(
2769
+ async (cart, path, content) => jsonOrThrow(
2770
+ await fetch(`${apiBaseUrl}/_console/dev/file`, {
2771
+ method: "POST",
2772
+ credentials: "include",
2773
+ headers: { "content-type": "application/json" },
2774
+ body: JSON.stringify({ cart, path, content })
2775
+ })
2776
+ ),
2777
+ [apiBaseUrl]
2778
+ );
2779
+ const applyCart = useCallback(
2780
+ async (cart) => jsonOrThrow(
2781
+ await fetch(`${apiBaseUrl}/_console/dev/apply`, {
2782
+ method: "POST",
2783
+ credentials: "include",
2784
+ headers: { "content-type": "application/json" },
2785
+ body: JSON.stringify({ cart })
2786
+ })
2787
+ ),
2788
+ [apiBaseUrl]
2789
+ );
2790
+ const scaffoldCart = useCallback(
2791
+ async (name) => jsonOrThrow(
2792
+ await fetch(`${apiBaseUrl}/_console/dev/scaffold`, {
2793
+ method: "POST",
2794
+ credentials: "include",
2795
+ headers: { "content-type": "application/json" },
2796
+ body: JSON.stringify({ name })
2797
+ })
2798
+ ),
2799
+ [apiBaseUrl]
2800
+ );
2801
+ return { listCarts, readFile, writeFile, applyCart, scaffoldCart };
2802
+ }
2803
+
2804
+ // src/components/devtools/sources/file-tree.tsx
2805
+ import React9 from "react";
2806
+ import { Lock, Plus, FilePlus2 } from "lucide-react";
2807
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
2808
+ var FileTree = ({
2809
+ carts,
2810
+ selected,
2811
+ dirtyKeys,
2812
+ onSelect,
2813
+ onNewCart,
2814
+ onNewMigration
2815
+ }) => {
2816
+ const [creating, setCreating] = React9.useState(false);
2817
+ const [newName, setNewName] = React9.useState("");
2818
+ const submitNew = () => {
2819
+ const name = newName.trim();
2820
+ if (/^[a-z][a-z0-9_]*$/.test(name)) {
2821
+ onNewCart(name);
2822
+ setNewName("");
2823
+ setCreating(false);
2824
+ }
2825
+ };
2826
+ return /* @__PURE__ */ jsxs8("div", { className: "flex h-full min-h-0 flex-col", children: [
2827
+ /* @__PURE__ */ jsxs8("div", { className: "flex h-6 shrink-0 items-center justify-between border-b border-border px-2", children: [
2828
+ /* @__PURE__ */ jsx13("span", { className: "text-[11px] font-medium text-muted-foreground", children: "Cartridges" }),
2829
+ /* @__PURE__ */ jsx13(
2830
+ "button",
2831
+ {
2832
+ type: "button",
2833
+ title: "New cartridge",
2834
+ onClick: () => setCreating((v) => !v),
2835
+ className: "rounded p-0.5 hover:bg-accent",
2836
+ children: /* @__PURE__ */ jsx13(Plus, { className: "size-3.5" })
2837
+ }
2838
+ )
2839
+ ] }),
2840
+ creating && /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-1 border-b border-border p-1", children: [
2841
+ /* @__PURE__ */ jsx13(
2842
+ "input",
2843
+ {
2844
+ autoFocus: true,
2845
+ value: newName,
2846
+ onChange: (e) => setNewName(e.target.value),
2847
+ onKeyDown: (e) => {
2848
+ if (e.key === "Enter") submitNew();
2849
+ if (e.key === "Escape") setCreating(false);
2850
+ },
2851
+ placeholder: "cart_name",
2852
+ className: "h-5 w-full rounded border border-border bg-background px-1 text-[11px] outline-none"
2853
+ }
2854
+ ),
2855
+ /* @__PURE__ */ jsx13(
2856
+ "button",
2857
+ {
2858
+ type: "button",
2859
+ onClick: submitNew,
2860
+ disabled: !/^[a-z][a-z0-9_]*$/.test(newName.trim()),
2861
+ className: "rounded px-1 text-[11px] hover:bg-accent disabled:opacity-40",
2862
+ children: "Create"
2863
+ }
2864
+ )
2865
+ ] }),
2866
+ /* @__PURE__ */ jsx13("div", { className: "min-h-0 flex-1 overflow-y-auto py-1", children: carts.map((cart) => /* @__PURE__ */ jsxs8("div", { className: "mb-1", children: [
2867
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between px-2 py-0.5", children: [
2868
+ /* @__PURE__ */ jsxs8("span", { className: "flex items-center gap-1 text-[11px] font-medium", children: [
2869
+ cart.name,
2870
+ !cart.writable && /* @__PURE__ */ jsx13(
2871
+ Lock,
2872
+ {
2873
+ className: "size-3 text-muted-foreground",
2874
+ "aria-label": cart.reason ?? "read-only"
2875
+ }
2876
+ )
2877
+ ] }),
2878
+ cart.writable && /* @__PURE__ */ jsx13(
2879
+ "button",
2880
+ {
2881
+ type: "button",
2882
+ title: "New migration",
2883
+ onClick: () => onNewMigration(cart.name),
2884
+ className: "rounded p-0.5 text-muted-foreground hover:bg-accent",
2885
+ children: /* @__PURE__ */ jsx13(FilePlus2, { className: "size-3" })
2886
+ }
2887
+ )
2888
+ ] }),
2889
+ cart.files.map((path) => {
2890
+ const key = `${cart.name}:${path}`;
2891
+ const isSel = selected?.cart === cart.name && selected?.path === path;
2892
+ return /* @__PURE__ */ jsxs8(
2893
+ "button",
2894
+ {
2895
+ type: "button",
2896
+ onClick: () => onSelect({ cart: cart.name, path }),
2897
+ "data-active": isSel,
2898
+ className: cn(
2899
+ "flex w-full items-center gap-1 truncate py-0.5 pl-4 pr-2 text-left text-[11px]",
2900
+ "hover:bg-accent data-[active=true]:bg-accent"
2901
+ ),
2902
+ children: [
2903
+ /* @__PURE__ */ jsx13("span", { className: "truncate", children: path }),
2904
+ dirtyKeys.has(key) && /* @__PURE__ */ jsx13("span", { className: "size-1.5 shrink-0 rounded-full bg-amber-400", title: "unsaved" })
2905
+ ]
2906
+ },
2907
+ path
2908
+ );
2909
+ })
2910
+ ] }, cart.name)) })
2911
+ ] });
2912
+ };
2913
+
2914
+ // src/components/devtools/sources/nbt-editor.tsx
2915
+ import React10 from "react";
2916
+ import { EditorState } from "@codemirror/state";
2917
+ import {
2918
+ EditorView as EditorView2,
2919
+ keymap as keymap2,
2920
+ lineNumbers,
2921
+ drawSelection,
2922
+ highlightActiveLine
2923
+ } from "@codemirror/view";
2924
+ import {
2925
+ defaultKeymap,
2926
+ history,
2927
+ historyKeymap,
2928
+ indentWithTab
2929
+ } from "@codemirror/commands";
2930
+ import {
2931
+ syntaxHighlighting,
2932
+ defaultHighlightStyle,
2933
+ bracketMatching
2934
+ } from "@codemirror/language";
2935
+ import { lintGutter } from "@codemirror/lint";
2936
+ import { searchKeymap } from "@codemirror/search";
2937
+ import { completionKeymap, closeBrackets } from "@codemirror/autocomplete";
2938
+
2939
+ // src/components/devtools/sources/nbt-language.ts
2940
+ import {
2941
+ StreamLanguage,
2942
+ LanguageSupport,
2943
+ indentUnit
2944
+ } from "@codemirror/language";
2945
+ var KEYWORDS = /* @__PURE__ */ new Set([
2946
+ "entity",
2947
+ "enum",
2948
+ "struct",
2949
+ "const",
2950
+ "export",
2951
+ "extends",
2952
+ "fn",
2953
+ "on",
2954
+ "action",
2955
+ "activity",
2956
+ "task",
2957
+ "variant",
2958
+ "workflow",
2959
+ "command",
2960
+ "middleware",
2961
+ "schedule",
2962
+ "every",
2963
+ "jai",
2964
+ "migration",
2965
+ "test",
2966
+ "mock",
2967
+ "assert",
2968
+ "cartridge",
2969
+ "service",
2970
+ "component",
2971
+ "app",
2972
+ "route",
2973
+ "layout",
2974
+ "import",
2975
+ "from",
2976
+ "if",
2977
+ "elif",
2978
+ "else",
2979
+ "while",
2980
+ "for",
2981
+ "in",
2982
+ "return",
2983
+ "break",
2984
+ "continue",
2985
+ "delete",
2986
+ "defer",
2987
+ "print",
2988
+ "sleep",
2989
+ "fail"
2990
+ ]);
2991
+ var TYPES = /* @__PURE__ */ new Set([
2992
+ "string",
2993
+ "bool",
2994
+ "ulid",
2995
+ "document",
2996
+ "dict",
2997
+ "blob",
2998
+ "DateTime",
2999
+ "u8",
3000
+ "u16",
3001
+ "u32",
3002
+ "u64",
3003
+ "s8",
3004
+ "s16",
3005
+ "s32",
3006
+ "s64",
3007
+ "int",
3008
+ "integer",
3009
+ "float",
3010
+ "float64",
3011
+ "f32",
3012
+ "f64",
3013
+ "double",
3014
+ "time",
3015
+ "bytes",
3016
+ "any",
3017
+ "name",
3018
+ "ref"
3019
+ ]);
3020
+ function eatSingleLineString(stream, quote) {
3021
+ while (!stream.eol()) {
3022
+ if (stream.next() === quote) break;
3023
+ }
3024
+ return "string";
3025
+ }
3026
+ var nbtParser = {
3027
+ name: "nbt",
3028
+ startState: () => ({ tripleString: false }),
3029
+ token(stream, state) {
3030
+ if (state.tripleString) {
3031
+ while (!stream.eol()) {
3032
+ if (stream.match('"""')) {
3033
+ state.tripleString = false;
3034
+ return "string";
3035
+ }
3036
+ stream.next();
3037
+ }
3038
+ return "string";
3039
+ }
3040
+ if (stream.eatSpace()) return null;
3041
+ const ch = stream.peek();
3042
+ if (ch == null) return null;
3043
+ if (ch === "#") {
3044
+ stream.skipToEnd();
3045
+ return "comment";
3046
+ }
3047
+ if (stream.match('"""')) {
3048
+ state.tripleString = true;
3049
+ while (!stream.eol()) {
3050
+ if (stream.match('"""')) {
3051
+ state.tripleString = false;
3052
+ return "string";
3053
+ }
3054
+ stream.next();
3055
+ }
3056
+ return "string";
3057
+ }
3058
+ if (ch === '"') {
3059
+ stream.next();
3060
+ return eatSingleLineString(stream, '"');
3061
+ }
3062
+ if (ch === "'") {
3063
+ const before = stream.string.charAt(stream.pos - 1);
3064
+ if (!/[A-Za-z0-9_]/.test(before)) {
3065
+ stream.next();
3066
+ return eatSingleLineString(stream, "'");
3067
+ }
3068
+ stream.next();
3069
+ return null;
3070
+ }
3071
+ if (ch === "f" && stream.string.charAt(stream.pos + 1) === '"') {
3072
+ stream.next();
3073
+ stream.next();
3074
+ return eatSingleLineString(stream, '"');
3075
+ }
3076
+ if (ch === "@") {
3077
+ stream.next();
3078
+ stream.eat("@");
3079
+ stream.eatWhile(/[A-Za-z0-9_]/);
3080
+ return "meta";
3081
+ }
3082
+ if (/\d/.test(ch)) {
3083
+ stream.match(/^\d+(\.\d+)?/);
3084
+ return "number";
3085
+ }
3086
+ if (/[A-Za-z_]/.test(ch)) {
3087
+ stream.eatWhile(/[A-Za-z0-9_]/);
3088
+ const word = stream.current();
3089
+ if (word === "true" || word === "false") return "atom";
3090
+ if (KEYWORDS.has(word)) return "keyword";
3091
+ if (TYPES.has(word)) return "typeName";
3092
+ if (/^[A-Z]/.test(word)) return "typeName";
3093
+ return "variableName";
3094
+ }
3095
+ if (/[+\-*/=<>!&|?.,:;]/.test(ch)) {
3096
+ stream.next();
3097
+ stream.eatWhile(/[+\-*/=<>!&|.]/);
3098
+ return "operator";
3099
+ }
3100
+ stream.next();
3101
+ return null;
3102
+ },
3103
+ languageData: {
3104
+ commentTokens: { line: "#" }
3105
+ }
3106
+ };
3107
+ var nbtLanguage = StreamLanguage.define(nbtParser);
3108
+ function nbtLanguageSupport() {
3109
+ return new LanguageSupport(nbtLanguage, [indentUnit.of(" ")]);
3110
+ }
3111
+
3112
+ // src/components/devtools/sources/lsp-extensions.ts
3113
+ import {
3114
+ ViewPlugin,
3115
+ hoverTooltip,
3116
+ keymap
3117
+ } from "@codemirror/view";
3118
+ import {
3119
+ autocompletion
3120
+ } from "@codemirror/autocomplete";
3121
+ import { setDiagnostics } from "@codemirror/lint";
3122
+ function toLspPos(doc, offset) {
3123
+ const line = doc.lineAt(offset);
3124
+ return { line: line.number - 1, character: offset - line.from };
3125
+ }
3126
+ function fromLspPos(doc, pos) {
3127
+ const lineNo = Math.min(Math.max(pos.line + 1, 1), doc.lines);
3128
+ const line = doc.line(lineNo);
3129
+ return Math.min(line.from + Math.max(pos.character, 0), line.to);
3130
+ }
3131
+ function toCmDiagnostics(doc, diags) {
3132
+ return diags.map((d) => {
3133
+ const from = fromLspPos(doc, d.range.start);
3134
+ let to = fromLspPos(doc, d.range.end);
3135
+ if (to <= from) to = Math.min(from + 1, doc.length);
3136
+ const severity = d.severity === 2 ? "warning" : d.severity === 3 || d.severity === 4 ? "info" : "error";
3137
+ return { from, to, severity, message: d.message, source: d.source ?? "nbt" };
3138
+ });
3139
+ }
3140
+ function completionType(kind) {
3141
+ switch (kind) {
3142
+ case 3:
3143
+ return "function";
3144
+ case 5:
3145
+ return "property";
3146
+ case 13:
3147
+ return "enum";
3148
+ case 14:
3149
+ return "keyword";
3150
+ case 22:
3151
+ return "class";
3152
+ default:
3153
+ return "variable";
3154
+ }
3155
+ }
3156
+ function lspExtensions(client, uri, onGotoDefinition) {
3157
+ const syncPlugin = ViewPlugin.fromClass(
3158
+ class {
3159
+ constructor(view) {
3160
+ this.view = view;
3161
+ this.timer = null;
3162
+ client.didOpen(uri, view.state.doc.toString());
3163
+ this.unsubscribe = client.onDiagnostics(uri, (diags) => {
3164
+ const cm = toCmDiagnostics(this.view.state.doc, diags);
3165
+ this.view.dispatch(setDiagnostics(this.view.state, cm));
3166
+ });
3167
+ }
3168
+ update(u) {
3169
+ if (!u.docChanged) return;
3170
+ if (this.timer) clearTimeout(this.timer);
3171
+ this.timer = setTimeout(() => {
3172
+ this.timer = null;
3173
+ client.didChange(uri, this.view.state.doc.toString());
3174
+ }, 200);
3175
+ }
3176
+ destroy() {
3177
+ if (this.timer) {
3178
+ clearTimeout(this.timer);
3179
+ client.didChange(uri, this.view.state.doc.toString());
3180
+ }
3181
+ this.unsubscribe();
3182
+ }
3183
+ }
3184
+ );
3185
+ const completionSource = async (ctx) => {
3186
+ const word = ctx.matchBefore(/\w*/);
3187
+ if (!word) return null;
3188
+ if (word.from === word.to && !ctx.explicit) {
3189
+ const before = ctx.state.doc.sliceString(Math.max(0, ctx.pos - 1), ctx.pos);
3190
+ if (before !== "." && before !== ":" && before !== '"') return null;
3191
+ }
3192
+ client.didChange(uri, ctx.state.doc.toString());
3193
+ let result;
3194
+ try {
3195
+ result = await client.completion(uri, toLspPos(ctx.state.doc, ctx.pos));
3196
+ } catch {
3197
+ return null;
3198
+ }
3199
+ const items = Array.isArray(result) ? result : result?.items ?? [];
3200
+ if (items.length === 0) return null;
3201
+ return {
3202
+ from: word.from,
3203
+ options: items.map((it) => ({
3204
+ label: it.label,
3205
+ type: completionType(it.kind),
3206
+ detail: it.detail
3207
+ }))
3208
+ };
3209
+ };
3210
+ const hover = hoverTooltip(async (view, pos) => {
3211
+ let result;
3212
+ try {
3213
+ result = await client.hover(uri, toLspPos(view.state.doc, pos));
3214
+ } catch {
3215
+ return null;
3216
+ }
3217
+ const value = result?.contents?.value;
3218
+ if (!value) return null;
3219
+ return {
3220
+ pos,
3221
+ create: () => {
3222
+ const dom = document.createElement("pre");
3223
+ dom.className = "nbt-hover-tooltip";
3224
+ dom.style.cssText = "margin:0;padding:6px 8px;max-width:480px;white-space:pre-wrap;font-size:11px;";
3225
+ dom.textContent = value.replace(/```\w*\n?/g, "");
3226
+ return { dom };
3227
+ }
3228
+ };
3229
+ });
3230
+ const gotoDef = keymap.of([
3231
+ {
3232
+ key: "F12",
3233
+ run: (view) => {
3234
+ const pos = toLspPos(view.state.doc, view.state.selection.main.head);
3235
+ client.definition(uri, pos).then((result) => {
3236
+ const loc = Array.isArray(result) ? result[0] ?? null : result;
3237
+ if (!loc?.uri) return;
3238
+ if (loc.uri === uri) {
3239
+ const off = fromLspPos(view.state.doc, loc.range.start);
3240
+ view.dispatch({
3241
+ selection: { anchor: off },
3242
+ scrollIntoView: true
3243
+ });
3244
+ } else {
3245
+ onGotoDefinition?.(loc.uri, loc.range.start.line);
3246
+ }
3247
+ }).catch(() => {
3248
+ });
3249
+ return true;
3250
+ }
3251
+ }
3252
+ ]);
3253
+ return [
3254
+ syncPlugin,
3255
+ autocompletion({ override: [completionSource] }),
3256
+ hover,
3257
+ gotoDef
3258
+ ];
3259
+ }
3260
+
3261
+ // src/components/devtools/sources/nbt-editor.tsx
3262
+ import { jsx as jsx14 } from "react/jsx-runtime";
3263
+ var nbtTheme = EditorView2.theme(
3264
+ {
3265
+ "&": {
3266
+ height: "100%",
3267
+ fontSize: "12px",
3268
+ backgroundColor: "var(--background)",
3269
+ color: "var(--foreground)"
3270
+ },
3271
+ ".cm-content": { fontFamily: "ui-monospace, monospace" },
3272
+ ".cm-gutters": {
3273
+ backgroundColor: "var(--background)",
3274
+ color: "var(--muted-foreground)",
3275
+ borderRight: "1px solid var(--border)"
3276
+ },
3277
+ ".cm-activeLine": { backgroundColor: "color-mix(in srgb, var(--accent) 35%, transparent)" },
3278
+ "&.cm-focused": { outline: "none" },
3279
+ ".cm-tooltip": {
3280
+ backgroundColor: "var(--popover)",
3281
+ color: "var(--popover-foreground)",
3282
+ border: "1px solid var(--border)"
3283
+ }
3284
+ },
3285
+ { dark: true }
3286
+ );
3287
+ var NbtEditor = ({
3288
+ uri,
3289
+ value,
3290
+ readOnly = false,
3291
+ lsp,
3292
+ onChange,
3293
+ onSave,
3294
+ onGotoDefinition
3295
+ }) => {
3296
+ const hostRef = React10.useRef(null);
3297
+ const viewRef = React10.useRef(null);
3298
+ const onChangeRef = React10.useRef(onChange);
3299
+ onChangeRef.current = onChange;
3300
+ const onSaveRef = React10.useRef(onSave);
3301
+ onSaveRef.current = onSave;
3302
+ const onGotoRef = React10.useRef(onGotoDefinition);
3303
+ onGotoRef.current = onGotoDefinition;
3304
+ React10.useEffect(() => {
3305
+ const host = hostRef.current;
3306
+ if (!host) return;
3307
+ const extensions = [
3308
+ lineNumbers(),
3309
+ history(),
3310
+ drawSelection(),
3311
+ highlightActiveLine(),
3312
+ bracketMatching(),
3313
+ closeBrackets(),
3314
+ lintGutter(),
3315
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
3316
+ nbtLanguageSupport(),
3317
+ nbtTheme,
3318
+ keymap2.of([
3319
+ {
3320
+ key: "Mod-s",
3321
+ run: (view2) => {
3322
+ onSaveRef.current?.(view2.state.doc.toString());
3323
+ return true;
3324
+ }
3325
+ },
3326
+ ...defaultKeymap,
3327
+ ...historyKeymap,
3328
+ ...searchKeymap,
3329
+ ...completionKeymap,
3330
+ indentWithTab
3331
+ ]),
3332
+ EditorView2.updateListener.of((u) => {
3333
+ if (u.docChanged) onChangeRef.current?.(u.state.doc.toString());
3334
+ }),
3335
+ EditorState.readOnly.of(readOnly)
3336
+ ];
3337
+ if (lsp && !readOnly) {
3338
+ extensions.push(
3339
+ lspExtensions(lsp, uri, (defUri, line) => onGotoRef.current?.(defUri, line))
3340
+ );
3341
+ }
3342
+ const view = new EditorView2({
3343
+ state: EditorState.create({ doc: value, extensions }),
3344
+ parent: host
3345
+ });
3346
+ viewRef.current = view;
3347
+ return () => {
3348
+ view.destroy();
3349
+ viewRef.current = null;
3350
+ };
3351
+ }, [uri, readOnly, lsp]);
3352
+ return /* @__PURE__ */ jsx14("div", { ref: hostRef, className: "h-full min-h-0 overflow-hidden" });
3353
+ };
3354
+
3355
+ // src/components/devtools/sources/lsp-client.ts
3356
+ var NbtLspClient = class {
3357
+ constructor(url) {
3358
+ this.rootUri = null;
3359
+ this.ws = null;
3360
+ this.nextId = 1;
3361
+ this.pending = /* @__PURE__ */ new Map();
3362
+ this.diagHandlers = /* @__PURE__ */ new Map();
3363
+ // Open buffers, kept for replay across reconnects.
3364
+ this.docs = /* @__PURE__ */ new Map();
3365
+ this.ready = false;
3366
+ this.queue = [];
3367
+ this.closed = false;
3368
+ this.retryMs = 500;
3369
+ this.url = url;
3370
+ this.connect();
3371
+ }
3372
+ dispose() {
3373
+ this.closed = true;
3374
+ this.ws?.close();
3375
+ this.pending.forEach((p) => p.reject(new Error("lsp client disposed")));
3376
+ this.pending.clear();
3377
+ }
3378
+ connect() {
3379
+ if (this.closed) return;
3380
+ const ws = new WebSocket(this.url);
3381
+ this.ws = ws;
3382
+ ws.onopen = () => {
3383
+ this.retryMs = 500;
3384
+ if (this.rootUri) this.sendInitialize(this.rootUri);
3385
+ };
3386
+ ws.onmessage = (ev) => {
3387
+ if (typeof ev.data !== "string") return;
3388
+ this.onMessage(ev.data);
3389
+ };
3390
+ ws.onclose = () => {
3391
+ this.ready = false;
3392
+ this.ws = null;
3393
+ for (const p of this.pending.values())
3394
+ p.reject(new Error("lsp connection closed"));
3395
+ this.pending.clear();
3396
+ if (!this.closed) {
3397
+ setTimeout(() => this.connect(), this.retryMs);
3398
+ this.retryMs = Math.min(this.retryMs * 2, 1e4);
3399
+ }
3400
+ };
3401
+ }
3402
+ // Called once by the host with file://<projectRoot>; also re-sent after every
3403
+ // reconnect, followed by didOpen replays for tracked buffers.
3404
+ initialize(rootUri) {
3405
+ this.rootUri = rootUri;
3406
+ if (this.ws?.readyState === WebSocket.OPEN) this.sendInitialize(rootUri);
3407
+ }
3408
+ sendInitialize(rootUri) {
3409
+ const id = this.nextId++;
3410
+ this.pending.set(id, {
3411
+ resolve: () => {
3412
+ this.sendRaw({ jsonrpc: "2.0", method: "initialized", params: {} });
3413
+ this.ready = true;
3414
+ for (const [uri, d] of this.docs) {
3415
+ this.sendRaw({
3416
+ jsonrpc: "2.0",
3417
+ method: "textDocument/didOpen",
3418
+ params: {
3419
+ textDocument: {
3420
+ uri,
3421
+ languageId: "nbt",
3422
+ version: d.version,
3423
+ text: d.text
3424
+ }
3425
+ }
3426
+ });
3427
+ }
3428
+ const q = this.queue;
3429
+ this.queue = [];
3430
+ for (const m of q) this.ws?.send(m);
3431
+ },
3432
+ reject: () => {
3433
+ }
3434
+ });
3435
+ this.ws?.send(
3436
+ JSON.stringify({
3437
+ jsonrpc: "2.0",
3438
+ id,
3439
+ method: "initialize",
3440
+ params: { rootUri }
3441
+ })
3442
+ );
3443
+ }
3444
+ sendRaw(msg) {
3445
+ const s = JSON.stringify(msg);
3446
+ if (this.ready && this.ws?.readyState === WebSocket.OPEN) this.ws.send(s);
3447
+ else this.queue.push(s);
3448
+ }
3449
+ onMessage(data) {
3450
+ let msg;
3451
+ try {
3452
+ msg = JSON.parse(data);
3453
+ } catch {
3454
+ return;
3455
+ }
3456
+ if (msg.id != null && (msg.result !== void 0 || msg.error)) {
3457
+ const p = this.pending.get(msg.id);
3458
+ if (!p) return;
3459
+ this.pending.delete(msg.id);
3460
+ if (msg.error) p.reject(new Error(msg.error.message ?? "lsp error"));
3461
+ else p.resolve(msg.result);
3462
+ return;
3463
+ }
3464
+ if (msg.method === "textDocument/publishDiagnostics") {
3465
+ const uri = msg.params?.uri;
3466
+ const handler = this.diagHandlers.get(uri);
3467
+ if (handler) handler(msg.params?.diagnostics ?? []);
3468
+ }
3469
+ }
3470
+ request(method, params) {
3471
+ const id = this.nextId++;
3472
+ const p = new Promise((resolve, reject) => {
3473
+ this.pending.set(id, { resolve, reject });
3474
+ });
3475
+ this.sendRaw({ jsonrpc: "2.0", id, method, params });
3476
+ return p;
3477
+ }
3478
+ onDiagnostics(uri, handler) {
3479
+ this.diagHandlers.set(uri, handler);
3480
+ return () => {
3481
+ if (this.diagHandlers.get(uri) === handler) this.diagHandlers.delete(uri);
3482
+ };
3483
+ }
3484
+ didOpen(uri, text) {
3485
+ const existing = this.docs.get(uri);
3486
+ if (existing) {
3487
+ this.didChange(uri, text);
3488
+ return;
3489
+ }
3490
+ this.docs.set(uri, { text, version: 1 });
3491
+ this.sendRaw({
3492
+ jsonrpc: "2.0",
3493
+ method: "textDocument/didOpen",
3494
+ params: { textDocument: { uri, languageId: "nbt", version: 1, text } }
3495
+ });
3496
+ }
3497
+ didChange(uri, text) {
3498
+ const d = this.docs.get(uri);
3499
+ if (!d) return this.didOpen(uri, text);
3500
+ if (d.text === text) return;
3501
+ d.text = text;
3502
+ d.version += 1;
3503
+ this.sendRaw({
3504
+ jsonrpc: "2.0",
3505
+ method: "textDocument/didChange",
3506
+ params: {
3507
+ textDocument: { uri, version: d.version },
3508
+ contentChanges: [{ text }]
3509
+ }
3510
+ });
3511
+ }
3512
+ didClose(uri) {
3513
+ if (!this.docs.delete(uri)) return;
3514
+ this.sendRaw({
3515
+ jsonrpc: "2.0",
3516
+ method: "textDocument/didClose",
3517
+ params: { textDocument: { uri } }
3518
+ });
3519
+ }
3520
+ completion(uri, pos) {
3521
+ return this.request("textDocument/completion", {
3522
+ textDocument: { uri },
3523
+ position: pos
3524
+ });
3525
+ }
3526
+ hover(uri, pos) {
3527
+ return this.request("textDocument/hover", {
3528
+ textDocument: { uri },
3529
+ position: pos
3530
+ });
3531
+ }
3532
+ definition(uri, pos) {
3533
+ return this.request("textDocument/definition", {
3534
+ textDocument: { uri },
3535
+ position: pos
3536
+ });
3537
+ }
3538
+ };
3539
+
3540
+ // src/components/devtools/sources/sources-tab.tsx
3541
+ import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
3542
+ var bufKey = (s) => `${s.cart}:${s.path}`;
3543
+ function migrationStamp() {
3544
+ const d = /* @__PURE__ */ new Date();
3545
+ const p = (n) => String(n).padStart(2, "0");
3546
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
3547
+ }
3548
+ var SourcesTab = () => {
3549
+ const { apiBaseUrl } = useDevToolsConfig();
3550
+ const status = useDevMode();
3551
+ const api = useDevFilesApi();
3552
+ const { sourcesCart, setSourcesCart, sourcesFile, setSourcesFile, sourcesTreeW, setSourcesTreeW } = useDevTools();
3553
+ const [carts, setCarts] = React11.useState([]);
3554
+ const [buffers, setBuffers] = React11.useState(/* @__PURE__ */ new Map());
3555
+ const [statusMsg, setStatusMsg] = React11.useState("");
3556
+ const [diags, setDiags] = React11.useState([]);
3557
+ const [applying, setApplying] = React11.useState(false);
3558
+ const selected = sourcesCart && sourcesFile ? { cart: sourcesCart, path: sourcesFile } : null;
3559
+ const lspRef = React11.useRef(null);
3560
+ React11.useEffect(() => {
3561
+ if (!status?.dev) return;
3562
+ const client = new NbtLspClient(`${wsBaseFrom(apiBaseUrl)}/_console/dev/lsp`);
3563
+ client.initialize(`file://${status.projectRoot}`);
3564
+ lspRef.current = client;
3565
+ return () => {
3566
+ client.dispose();
3567
+ lspRef.current = null;
3568
+ };
3569
+ }, [status?.dev, status?.projectRoot, apiBaseUrl]);
3570
+ const refreshCarts = React11.useCallback(() => {
3571
+ api.listCarts().then(setCarts).catch((e) => setStatusMsg(String(e.message ?? e)));
3572
+ }, [api]);
3573
+ React11.useEffect(() => {
3574
+ if (status?.dev) refreshCarts();
3575
+ }, [status?.dev, refreshCarts]);
3576
+ const cartOf = React11.useCallback(
3577
+ (name) => carts.find((c) => c.name === name),
3578
+ [carts]
3579
+ );
3580
+ const openFile = React11.useCallback(
3581
+ (sel) => {
3582
+ setSourcesCart(sel.cart);
3583
+ setSourcesFile(sel.path);
3584
+ setDiags([]);
3585
+ setStatusMsg("");
3586
+ const key = bufKey(sel);
3587
+ if (buffers.has(key)) return;
3588
+ api.readFile(sel.cart, sel.path).then(({ content, writable }) => {
3589
+ setBuffers((prev) => {
3590
+ if (prev.has(key)) return prev;
3591
+ const next = new Map(prev);
3592
+ next.set(key, { saved: content, current: content, writable });
3593
+ return next;
3594
+ });
3595
+ }).catch((e) => setStatusMsg(String(e.message ?? e)));
3596
+ },
3597
+ [api, buffers, setSourcesCart, setSourcesFile]
3598
+ );
3599
+ React11.useEffect(() => {
3600
+ if (selected && carts.length > 0 && !buffers.has(bufKey(selected))) openFile(selected);
3601
+ }, [carts]);
3602
+ const buf = selected ? buffers.get(bufKey(selected)) : void 0;
3603
+ const dirtyKeys = React11.useMemo(() => {
3604
+ const s = /* @__PURE__ */ new Set();
3605
+ for (const [k, b] of buffers) if (b.current !== b.saved) s.add(k);
3606
+ return s;
3607
+ }, [buffers]);
3608
+ React11.useEffect(() => {
3609
+ if (dirtyKeys.size === 0) return;
3610
+ const onBeforeUnload = (e) => e.preventDefault();
3611
+ window.addEventListener("beforeunload", onBeforeUnload);
3612
+ return () => window.removeEventListener("beforeunload", onBeforeUnload);
3613
+ }, [dirtyKeys.size]);
3614
+ const onChange = React11.useCallback(
3615
+ (text) => {
3616
+ if (!selected) return;
3617
+ const key = bufKey(selected);
3618
+ setBuffers((prev) => {
3619
+ const b = prev.get(key);
3620
+ if (!b || b.current === text) return prev;
3621
+ const next = new Map(prev);
3622
+ next.set(key, { ...b, current: text });
3623
+ return next;
3624
+ });
3625
+ },
3626
+ [selected]
3627
+ );
3628
+ const onSave = React11.useCallback(
3629
+ (text) => {
3630
+ if (!selected) return;
3631
+ const key = bufKey(selected);
3632
+ api.writeFile(selected.cart, selected.path, text).then((r) => {
3633
+ setDiags(r.diagnostics);
3634
+ setStatusMsg(
3635
+ r.ok ? `saved ${selected.path}` : `saved ${selected.path} \u2014 ${r.diagnostics.length} problem(s)`
3636
+ );
3637
+ setBuffers((prev) => {
3638
+ const b = prev.get(key);
3639
+ if (!b) return prev;
3640
+ const next = new Map(prev);
3641
+ next.set(key, { ...b, saved: text, current: text });
3642
+ return next;
3643
+ });
3644
+ refreshCarts();
3645
+ }).catch((e) => setStatusMsg(`save failed: ${e.message ?? e}`));
3646
+ },
3647
+ [api, selected, refreshCarts]
3648
+ );
3649
+ const onApply = React11.useCallback(() => {
3650
+ if (!selected) return;
3651
+ setApplying(true);
3652
+ setStatusMsg(`applying ${selected.cart}\u2026`);
3653
+ api.applyCart(selected.cart).then(() => setStatusMsg(`applied ${selected.cart} \u2014 pending migrations ran, cart re-registered`)).catch((e) => setStatusMsg(`apply failed: ${e.message ?? e}`)).finally(() => setApplying(false));
3654
+ }, [api, selected]);
3655
+ const onNewCart = React11.useCallback(
3656
+ (name) => {
3657
+ setStatusMsg(`creating ${name}\u2026`);
3658
+ api.scaffoldCart(name).then(() => {
3659
+ setStatusMsg(`created ${name}`);
3660
+ refreshCarts();
3661
+ openFile({ cart: name, path: "schema.nbt" });
3662
+ }).catch((e) => setStatusMsg(`scaffold failed: ${e.message ?? e}`));
3663
+ },
3664
+ [api, refreshCarts, openFile]
3665
+ );
3666
+ const onNewMigration = React11.useCallback(
3667
+ (cart2) => {
3668
+ const path = `migrations/${migrationStamp()}_change/migration.nbt`;
3669
+ const key = `${cart2}:${path}`;
3670
+ setBuffers((prev) => {
3671
+ const next = new Map(prev);
3672
+ next.set(key, {
3673
+ saved: "",
3674
+ current: "migration change {\n \n}\n",
3675
+ writable: true
3676
+ });
3677
+ return next;
3678
+ });
3679
+ setSourcesCart(cart2);
3680
+ setSourcesFile(path);
3681
+ },
3682
+ [setSourcesCart, setSourcesFile]
3683
+ );
3684
+ const onGotoDefinition = React11.useCallback(
3685
+ (uri, _line) => {
3686
+ const path = uri.replace(/^file:\/\//, "");
3687
+ let best = null;
3688
+ for (const c of carts) {
3689
+ if (!c.sourceDir) continue;
3690
+ const prefix = `${c.sourceDir}/`;
3691
+ if (path.startsWith(prefix) && (!best || prefix.length > best.cart.length)) {
3692
+ best = { cart: c.name, rel: path.slice(prefix.length) };
3693
+ }
3694
+ }
3695
+ if (best) openFile({ cart: best.cart, path: best.rel });
3696
+ },
3697
+ [carts, openFile]
3698
+ );
3699
+ const startTreeResize = React11.useCallback(
3700
+ (e) => {
3701
+ e.preventDefault();
3702
+ const startX = e.clientX;
3703
+ const startW = sourcesTreeW;
3704
+ const handle = e.currentTarget;
3705
+ handle.setPointerCapture(e.pointerId);
3706
+ const onMove = (ev) => setSourcesTreeW(Math.max(120, Math.min(480, startW + (ev.clientX - startX))));
3707
+ const onUp = () => {
3708
+ handle.removeEventListener("pointermove", onMove);
3709
+ handle.removeEventListener("pointerup", onUp);
3710
+ };
3711
+ handle.addEventListener("pointermove", onMove);
3712
+ handle.addEventListener("pointerup", onUp);
3713
+ },
3714
+ [sourcesTreeW, setSourcesTreeW]
3715
+ );
3716
+ if (!status?.dev) {
3717
+ return /* @__PURE__ */ jsx15("div", { className: "flex h-full items-center justify-center text-[12px] text-muted-foreground", children: "Sources requires the console to run in dev mode (`console up --dev`)." });
3718
+ }
3719
+ const cart = selected ? cartOf(selected.cart) : void 0;
3720
+ const editorUri = selected && cart?.sourceDir ? `file://${cart.sourceDir}/${selected.path}` : selected ? `file://${status.projectRoot}/.readonly/${selected.cart}/${selected.path}` : "";
3721
+ const isDirty = selected ? dirtyKeys.has(bufKey(selected)) : false;
3722
+ return /* @__PURE__ */ jsxs9("div", { className: "flex h-full min-h-0 flex-col", children: [
3723
+ /* @__PURE__ */ jsxs9("div", { className: "flex min-h-0 flex-1", children: [
3724
+ /* @__PURE__ */ jsx15("div", { style: { width: sourcesTreeW }, className: "shrink-0 border-r border-border", children: /* @__PURE__ */ jsx15(
3725
+ FileTree,
3726
+ {
3727
+ carts,
3728
+ selected,
3729
+ dirtyKeys,
3730
+ onSelect: openFile,
3731
+ onNewCart,
3732
+ onNewMigration
3733
+ }
3734
+ ) }),
3735
+ /* @__PURE__ */ jsx15(
3736
+ "div",
3737
+ {
3738
+ onPointerDown: startTreeResize,
3739
+ style: { touchAction: "none" },
3740
+ className: "w-1 shrink-0 cursor-ew-resize bg-transparent hover:bg-accent/40"
3741
+ }
3742
+ ),
3743
+ /* @__PURE__ */ jsx15("div", { className: "min-h-0 min-w-0 flex-1", children: selected && buf ? /* @__PURE__ */ jsx15(
3744
+ NbtEditor,
3745
+ {
3746
+ uri: editorUri,
3747
+ value: buf.current,
3748
+ readOnly: !buf.writable,
3749
+ lsp: lspRef.current,
3750
+ onChange,
3751
+ onSave,
3752
+ onGotoDefinition
3753
+ },
3754
+ bufKey(selected)
3755
+ ) : /* @__PURE__ */ jsx15("div", { className: "flex h-full items-center justify-center text-[12px] text-muted-foreground", children: "Select a file \u2014 or create a cartridge with the + button." }) })
3756
+ ] }),
3757
+ /* @__PURE__ */ jsxs9("div", { className: "flex h-6 shrink-0 items-center gap-2 border-t border-border px-2 text-[11px]", children: [
3758
+ selected && buf?.writable && /* @__PURE__ */ jsxs9(Fragment4, { children: [
3759
+ /* @__PURE__ */ jsxs9(
3760
+ "button",
3761
+ {
3762
+ type: "button",
3763
+ onClick: () => buf && onSave(buf.current),
3764
+ className: cn(
3765
+ "flex items-center gap-1 rounded px-1.5 py-0.5 hover:bg-accent",
3766
+ isDirty && "text-amber-400"
3767
+ ),
3768
+ title: "Save (Ctrl+S) \u2014 mirrors to your local project folder",
3769
+ children: [
3770
+ /* @__PURE__ */ jsx15(Save, { className: "size-3" }),
3771
+ " Save"
3772
+ ]
3773
+ }
3774
+ ),
3775
+ /* @__PURE__ */ jsxs9(
3776
+ "button",
3777
+ {
3778
+ type: "button",
3779
+ onClick: onApply,
3780
+ disabled: applying || isDirty,
3781
+ className: "flex items-center gap-1 rounded px-1.5 py-0.5 hover:bg-accent disabled:opacity-40",
3782
+ title: isDirty ? "Save first" : "Re-install this cart from disk (runs your pending migrations)",
3783
+ children: [
3784
+ /* @__PURE__ */ jsx15(Play2, { className: "size-3" }),
3785
+ " Apply"
3786
+ ]
3787
+ }
3788
+ )
3789
+ ] }),
3790
+ selected && !buf?.writable && /* @__PURE__ */ jsxs9("span", { className: "text-muted-foreground", children: [
3791
+ "read-only (",
3792
+ cart?.reason ?? "no source mapping",
3793
+ ")"
3794
+ ] }),
3795
+ /* @__PURE__ */ jsx15("span", { className: "min-w-0 flex-1 truncate text-muted-foreground", children: statusMsg }),
3796
+ diags.length > 0 && /* @__PURE__ */ jsxs9("span", { className: "shrink-0 text-red-400", title: diags.map((d) => `${d.line}:${d.col} ${d.message}`).join("\n"), children: [
3797
+ diags.length,
3798
+ " problem",
3799
+ diags.length > 1 ? "s" : ""
3800
+ ] })
3801
+ ] })
3802
+ ] });
3803
+ };
3804
+ var sources_tab_default = SourcesTab;
3805
+
2171
3806
  // src/components/devtools/dev-tools.tsx
2172
- import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
3807
+ import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
2173
3808
  var MIN_H = 120;
2174
3809
  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, {}) }
3810
+ var EDGE = 8;
3811
+ var BASE_TABS = [
3812
+ { id: "console", title: "Console", render: () => /* @__PURE__ */ jsx16(console_tab_default, {}) },
3813
+ { id: "network", title: "Network", render: () => /* @__PURE__ */ jsx16(network_tab_default, {}) },
3814
+ { id: "data", title: "Data", render: () => /* @__PURE__ */ jsx16(data_tab_default, {}) }
2180
3815
  ];
3816
+ var SOURCES_TAB = {
3817
+ id: "sources",
3818
+ title: "Sources",
3819
+ render: () => /* @__PURE__ */ jsx16(sources_tab_default, {})
3820
+ };
3821
+ var SETTINGS_TAB = {
3822
+ id: "settings",
3823
+ title: "Settings",
3824
+ render: () => /* @__PURE__ */ jsx16(settings_tab_default, {})
3825
+ };
2181
3826
  var DevTools = () => {
2182
3827
  const {
2183
3828
  open,
@@ -2191,21 +3836,66 @@ var DevTools = () => {
2191
3836
  maximized,
2192
3837
  setMaximized
2193
3838
  } = useDevTools();
2194
- const panelRef = React8.useRef(null);
2195
- React8.useEffect(() => {
3839
+ const panelRef = React12.useRef(null);
3840
+ const hasOpenedRef = React12.useRef(false);
3841
+ const devMode = useDevMode();
3842
+ const tabs = React12.useMemo(
3843
+ () => devMode?.dev ? [...BASE_TABS, SOURCES_TAB, SETTINGS_TAB] : [...BASE_TABS, SETTINGS_TAB],
3844
+ [devMode?.dev]
3845
+ );
3846
+ const [vp, setVp] = React12.useState(
3847
+ () => typeof window === "undefined" ? { w: 0, h: 0 } : { w: window.innerWidth, h: window.innerHeight }
3848
+ );
3849
+ React12.useEffect(() => {
3850
+ const onResize = () => setVp({ w: window.innerWidth, h: window.innerHeight });
3851
+ window.addEventListener("resize", onResize);
3852
+ window.visualViewport?.addEventListener("resize", onResize);
3853
+ return () => {
3854
+ window.removeEventListener("resize", onResize);
3855
+ window.visualViewport?.removeEventListener("resize", onResize);
3856
+ };
3857
+ }, []);
3858
+ React12.useEffect(() => {
2196
3859
  if (activeTab) return;
2197
- if (DEV_TOOLS_TABS.length > 0) setActiveTab(DEV_TOOLS_TABS[0].id);
2198
- }, [activeTab, setActiveTab]);
2199
- const startResize = React8.useCallback(
3860
+ if (tabs.length > 0) setActiveTab(tabs[0].id);
3861
+ }, [activeTab, setActiveTab, tabs]);
3862
+ React12.useEffect(() => {
3863
+ const panel = panelRef.current;
3864
+ if (!panel) return;
3865
+ const onWheel = (e) => {
3866
+ let node = e.target;
3867
+ while (node && node !== panel) {
3868
+ const s = getComputedStyle(node);
3869
+ const vertical = Math.abs(e.deltaY) >= Math.abs(e.deltaX);
3870
+ if (vertical) {
3871
+ const scrollable = (s.overflowY === "auto" || s.overflowY === "scroll") && node.scrollHeight > node.clientHeight;
3872
+ if (scrollable) {
3873
+ const atTop = node.scrollTop <= 0;
3874
+ const atBottom = node.scrollTop + node.clientHeight >= node.scrollHeight - 1;
3875
+ if (!(e.deltaY < 0 && atTop || e.deltaY > 0 && atBottom)) return;
3876
+ }
3877
+ } else {
3878
+ const scrollable = (s.overflowX === "auto" || s.overflowX === "scroll") && node.scrollWidth > node.clientWidth;
3879
+ if (scrollable) {
3880
+ const atLeft = node.scrollLeft <= 0;
3881
+ const atRight = node.scrollLeft + node.clientWidth >= node.scrollWidth - 1;
3882
+ if (!(e.deltaX < 0 && atLeft || e.deltaX > 0 && atRight)) return;
3883
+ }
3884
+ }
3885
+ node = node.parentElement;
3886
+ }
3887
+ e.preventDefault();
3888
+ };
3889
+ panel.addEventListener("wheel", onWheel, { passive: false });
3890
+ return () => panel.removeEventListener("wheel", onWheel);
3891
+ }, [open]);
3892
+ const startResize = React12.useCallback(
2200
3893
  (e) => {
2201
3894
  e.preventDefault();
2202
3895
  const panel = panelRef.current;
2203
3896
  if (!panel) return;
2204
- const parent = panel.parentElement;
2205
- if (!parent) return;
2206
3897
  const handle = e.currentTarget;
2207
3898
  handle.setPointerCapture(e.pointerId);
2208
- const parentRect = parent.getBoundingClientRect();
2209
3899
  const startX = e.clientX;
2210
3900
  const startY = e.clientY;
2211
3901
  const startH = panel.offsetHeight;
@@ -2229,13 +3919,13 @@ var DevTools = () => {
2229
3919
  const dy = startY - ev.clientY;
2230
3920
  pendingNext = Math.max(
2231
3921
  MIN_H,
2232
- Math.min(parentRect.height - 24, startH + dy)
3922
+ Math.min(window.innerHeight - 24, startH + dy)
2233
3923
  );
2234
3924
  } else {
2235
3925
  const dx = startX - ev.clientX;
2236
3926
  pendingNext = Math.max(
2237
3927
  MIN_W,
2238
- Math.min(parentRect.width - 24, startW + dx)
3928
+ Math.min(window.innerWidth - 24, startW + dx)
2239
3929
  );
2240
3930
  }
2241
3931
  if (!rafId) rafId = requestAnimationFrame(flush);
@@ -2264,81 +3954,93 @@ var DevTools = () => {
2264
3954
  },
2265
3955
  [dock, setSize]
2266
3956
  );
2267
- if (!open) return null;
2268
- const active = DEV_TOOLS_TABS.find((t) => t.id === activeTab) ?? DEV_TOOLS_TABS[0];
3957
+ if (open) hasOpenedRef.current = true;
3958
+ if (!hasOpenedRef.current) return null;
3959
+ const active = tabs.find((t) => t.id === activeTab) ?? tabs[0];
2269
3960
  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
- }
3961
+ const sizeStyle = maximized ? {} : dock === "bottom" ? { height: Math.max(MIN_H, Math.min(size.h, vp.h - EDGE)) } : { width: Math.max(MIN_W, Math.min(size.w, vp.w - EDGE)) };
3962
+ return createPortal2(
3963
+ /* @__PURE__ */ jsxs10(
3964
+ "div",
3965
+ {
3966
+ ref: panelRef,
3967
+ style: sizeStyle,
3968
+ className: cn(
3969
+ "nimbit-devtools dark fixed z-50 flex flex-col border-border bg-background text-foreground text-[12px] shadow-lg",
3970
+ positionClass,
3971
+ dock === "bottom" ? "border-t" : "border-l",
3972
+ !open && "hidden"
2293
3973
  ),
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
- }
3974
+ onMouseDown: (e) => e.stopPropagation(),
3975
+ children: [
3976
+ !maximized && /* @__PURE__ */ jsx16(
3977
+ "div",
3978
+ {
3979
+ onPointerDown: startResize,
3980
+ style: { touchAction: "none" },
3981
+ className: cn(
3982
+ "absolute z-10 bg-transparent hover:bg-accent/40",
3983
+ 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"
3984
+ )
3985
+ }
3986
+ ),
3987
+ /* @__PURE__ */ jsxs10("div", { className: "flex h-7 shrink-0 items-center border-b border-border bg-background pl-1 pr-1 select-none", children: [
3988
+ /* @__PURE__ */ jsx16("div", { className: "flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto", children: tabs.map((tab) => {
3989
+ const isActive = tab.id === active?.id;
3990
+ return /* @__PURE__ */ jsx16(
3991
+ "button",
3992
+ {
3993
+ type: "button",
3994
+ "data-active": isActive,
3995
+ onClick: () => setActiveTab(tab.id),
3996
+ className: cn(
3997
+ "h-5 shrink-0 rounded-md px-2 text-[12px] leading-none outline-none transition-colors",
3998
+ "text-foreground hover:bg-accent hover:text-accent-foreground",
3999
+ "data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
4000
+ ),
4001
+ children: /* @__PURE__ */ jsxs10("span", { className: "inline-flex items-center gap-1", children: [
4002
+ tab.icon,
4003
+ tab.title
4004
+ ] })
4005
+ },
4006
+ tab.id
4007
+ );
4008
+ }) }),
4009
+ /* @__PURE__ */ jsxs10("div", { className: "flex shrink-0 items-center gap-0.5 pl-2", children: [
4010
+ /* @__PURE__ */ jsx16(
4011
+ IconBtn,
4012
+ {
4013
+ label: dock === "bottom" ? "Dock right" : "Dock bottom",
4014
+ onClick: () => setDock(dock === "bottom" ? "right" : "bottom"),
4015
+ children: dock === "bottom" ? /* @__PURE__ */ jsx16(PanelRight, { className: "size-3.5" }) : /* @__PURE__ */ jsx16(PanelBottom, { className: "size-3.5" })
4016
+ }
4017
+ ),
4018
+ /* @__PURE__ */ jsx16(
4019
+ IconBtn,
4020
+ {
4021
+ label: maximized ? "Restore" : "Maximize",
4022
+ onClick: () => setMaximized(!maximized),
4023
+ children: maximized ? /* @__PURE__ */ jsx16(Minimize2, { className: "size-3.5" }) : /* @__PURE__ */ jsx16(Maximize2, { className: "size-3.5" })
4024
+ }
4025
+ ),
4026
+ /* @__PURE__ */ jsx16(IconBtn, { label: "Close", onClick: () => setOpen(false), children: /* @__PURE__ */ jsx16(X2, { className: "size-3.5" }) })
4027
+ ] })
4028
+ ] }),
4029
+ /* @__PURE__ */ jsx16("div", { className: "min-h-0 min-w-0 flex-1 overflow-hidden", children: tabs.map((tab) => /* @__PURE__ */ jsx16(
4030
+ "div",
4031
+ {
4032
+ className: cn("h-full w-full", tab.id !== active?.id && "hidden"),
4033
+ children: tab.render()
4034
+ },
4035
+ tab.id
4036
+ )) })
4037
+ ]
4038
+ }
4039
+ ),
4040
+ document.body
2339
4041
  );
2340
4042
  };
2341
- var IconBtn = ({ label, className, children, ...rest }) => /* @__PURE__ */ jsx12(
4043
+ var IconBtn = ({ label, className, children, ...rest }) => /* @__PURE__ */ jsx16(
2342
4044
  "button",
2343
4045
  {
2344
4046
  type: "button",
@@ -2355,11 +4057,11 @@ var IconBtn = ({ label, className, children, ...rest }) => /* @__PURE__ */ jsx12
2355
4057
  var dev_tools_default = DevTools;
2356
4058
 
2357
4059
  // src/index.tsx
2358
- import { jsx as jsx13 } from "react/jsx-runtime";
4060
+ import { jsx as jsx17 } from "react/jsx-runtime";
2359
4061
  var NimbitDevTools = ({
2360
4062
  apiBaseUrl = "",
2361
4063
  defaultActiveTab
2362
- }) => /* @__PURE__ */ jsx13(DevToolsConfigProvider, { apiBaseUrl, children: /* @__PURE__ */ jsx13(DevToolsProvider, { defaultActiveTab, children: /* @__PURE__ */ jsx13(dev_tools_default, {}) }) });
4064
+ }) => /* @__PURE__ */ jsx17(DevToolsConfigProvider, { apiBaseUrl, children: /* @__PURE__ */ jsx17(DevToolsProvider, { defaultActiveTab, children: /* @__PURE__ */ jsx17(dev_tools_default, {}) }) });
2363
4065
  var index_default = NimbitDevTools;
2364
4066
  function toggleDevTools() {
2365
4067
  window.dispatchEvent(new CustomEvent("devtools-toggle"));
@@ -2368,8 +4070,10 @@ export {
2368
4070
  dev_tools_default as DevTools,
2369
4071
  DevToolsConfigProvider,
2370
4072
  DevToolsProvider,
4073
+ NbtEditor,
2371
4074
  NimbitDevTools,
2372
4075
  index_default as default,
4076
+ nbtLanguageSupport,
2373
4077
  toggleDevTools,
2374
4078
  useDevTools,
2375
4079
  useDevToolsConfig