@pythia-software/query-table-ui 0.9.7 → 0.9.8

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
@@ -389,7 +389,8 @@ function metricFormatFromMeasure(clause, measure) {
389
389
  }
390
390
 
391
391
  // src/JsonCell.tsx
392
- import { useEffect as useEffect3, useMemo, useRef as useRef2, useState as useState3 } from "react";
392
+ import { useEffect as useEffect3, useLayoutEffect as useLayoutEffect2, useMemo, useRef as useRef2, useState as useState3 } from "react";
393
+ import { jsonInspectionScopeKey } from "@pythia-software/query-table-core";
393
394
 
394
395
  // src/AdaptiveOverlay.tsx
395
396
  import {
@@ -467,7 +468,8 @@ function ModalSurface({
467
468
  chrome = true,
468
469
  returnFocus,
469
470
  backdropClassName,
470
- initialFocus
471
+ initialFocus,
472
+ preserveFocus = false
471
473
  }) {
472
474
  const mobile = useMobileLayout();
473
475
  const titleId = useId();
@@ -506,7 +508,7 @@ function ModalSurface({
506
508
  document.body.style.overflow = "hidden";
507
509
  }
508
510
  modalStack.push(element);
509
- (element.querySelector(initialFocus ?? ":not(*)") ?? element).focus();
511
+ if (!preserveFocus) (element.querySelector(initialFocus ?? ":not(*)") ?? element).focus();
510
512
  const isTop = () => modalStack[modalStack.length - 1] === element;
511
513
  const focusable = () => Array.from(
512
514
  element.querySelectorAll(
@@ -551,7 +553,7 @@ function ModalSurface({
551
553
  }
552
554
  };
553
555
  const onFocus = (event) => {
554
- if (isTop() && !element.contains(event.target)) element.focus();
556
+ if (!preserveFocus && isTop() && !element.contains(event.target)) element.focus();
555
557
  };
556
558
  document.addEventListener("keydown", onKey);
557
559
  document.addEventListener("focusin", onFocus);
@@ -562,7 +564,7 @@ function ModalSurface({
562
564
  if (modalStack.length === 0)
563
565
  document.body.style.overflow = originalOverflow;
564
566
  const destination = focusDestination.current ?? previous;
565
- if (destination?.isConnected) destination.focus({ preventScroll: true });
567
+ if (!preserveFocus && destination?.isConnected) destination.focus({ preventScroll: true });
566
568
  };
567
569
  }, [mobile]);
568
570
  const content = /* @__PURE__ */ jsx2(
@@ -853,9 +855,37 @@ function jsonSearchMatches(entries, search) {
853
855
 
854
856
  // src/JsonCell.tsx
855
857
  import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
856
- function JsonCell({ value, label = "JSON", display: display2 }) {
857
- const [open, setOpen] = useState3(false);
858
+ function JsonCell(props) {
859
+ const { value, label = "JSON", display: display2 } = props;
858
860
  const inspection = useMemo(() => inspectJsonValue(value, display2, false), [value, display2]);
861
+ const [state, change] = useInspectionState(props, inspection);
862
+ const preserveFocus = useRef2(false);
863
+ const latest = useRef2({ state, change, controlled: props.inspectionState !== void 0 });
864
+ latest.current = { state, change, controlled: props.inspectionState !== void 0 };
865
+ const initialized = useRef2(null);
866
+ const sessionKey = props.inspectionScope ? jsonInspectionScopeKey(props.inspectionScope) : inspection.raw + JSON.stringify(display2);
867
+ const openInspector = (preserve, full = inspectJsonValue(value, display2)) => {
868
+ const current = latest.current;
869
+ const next = current.controlled || initialized.current === sessionKey ? current.state : createJsonInspectionState(full, display2);
870
+ initialized.current = sessionKey;
871
+ preserveFocus.current = preserve;
872
+ current.change({ ...next, open: true });
873
+ };
874
+ const scopeKey = props.inspectionScope ? jsonInspectionScopeKey(props.inspectionScope) : "";
875
+ useEffect3(() => {
876
+ if (!props.attention || !props.inspectionScope || state.open || value == null || value === "") return;
877
+ const registry = props.attention;
878
+ const target = props.inspectionScope;
879
+ const provider = { reveal: async (destination, signal) => {
880
+ if (signal.aborted) return false;
881
+ const full = inspectJsonValue(value, display2);
882
+ if (destination.path && !full.entries.some((entry) => entry.path === destination.path)) return false;
883
+ openInspector(true, full);
884
+ const mounted = await registry.whenRegistered(target, signal, provider);
885
+ return signal.aborted ? false : mounted.reveal(destination, signal);
886
+ } };
887
+ return registry.register(target, provider);
888
+ }, [props.attention, scopeKey, state.open, inspection]);
859
889
  if (value == null || value === "") return /* @__PURE__ */ jsx3("span", { className: "qt-muted", children: "\u2014" });
860
890
  return /* @__PURE__ */ jsxs3("div", { className: "qt-json-cell", children: [
861
891
  /* @__PURE__ */ jsx3(
@@ -866,28 +896,54 @@ function JsonCell({ value, label = "JSON", display: display2 }) {
866
896
  title: inspection.error ?? "Open in JSON Viewer",
867
897
  "aria-label": `Inspect ${label}`,
868
898
  "aria-haspopup": "dialog",
899
+ "aria-expanded": state.open,
869
900
  onClick: (event) => {
870
901
  event.stopPropagation();
871
- setOpen(true);
902
+ openInspector(false);
872
903
  },
873
904
  onDoubleClick: (event) => event.stopPropagation(),
874
905
  children: /* @__PURE__ */ jsx3("span", { className: "qt-json-badge", children: inspection.error ? "JSON?" : "JSON" })
875
906
  }
876
907
  ),
877
908
  /* @__PURE__ */ jsx3("code", { className: "qt-json-preview-text", title: inspection.error ?? inspection.preview, children: inspection.preview }),
878
- open && /* @__PURE__ */ jsx3(JsonViewerModal, { value, label, display: display2, onClose: () => setOpen(false) })
909
+ state.open && /* @__PURE__ */ jsx3(JsonViewerModal, { ...props, preserveFocus: preserveFocus.current, inspectionState: state, onInspectionStateChange: change, onClose: () => change({ ...state, open: false }) })
879
910
  ] });
880
911
  }
881
- function JsonViewerModal({ value, label = "JSON", display: display2, onClose, returnFocus }) {
912
+ function JsonViewerModal({ onClose, returnFocus, preserveFocus, ...props }) {
913
+ const { value, label = "JSON", display: display2 } = props;
882
914
  const inspection = useMemo(() => inspectJsonValue(value, display2), [value, display2]);
883
- return /* @__PURE__ */ jsx3("span", { onClick: (event) => event.stopPropagation(), onDoubleClick: (event) => event.stopPropagation(), onContextMenu: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsx3(ModalSurface, { title: `${label} \xB7 JSON`, onClose, returnFocus, chrome: false, className: "qt-modal qt-json-inspector", children: /* @__PURE__ */ jsx3(JsonValueContent, { inspection, label, display: display2, onClose }) }) });
915
+ const [state, change] = useInspectionState(props, inspection);
916
+ return /* @__PURE__ */ jsx3("span", { onClick: (event) => event.stopPropagation(), onDoubleClick: (event) => event.stopPropagation(), onContextMenu: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsx3(ModalSurface, { title: `${label} \xB7 JSON`, onClose, returnFocus, preserveFocus, chrome: false, className: "qt-modal qt-json-inspector", children: /* @__PURE__ */ jsx3(JsonValueContent, { ...props, inspection, state, change, label, onClose }) }) });
917
+ }
918
+ function createJsonInspectionState(inspection, display2, compact = false) {
919
+ return {
920
+ version: 1,
921
+ open: false,
922
+ search: "",
923
+ raw: Boolean(inspection.error),
924
+ visibleEntryLimit: 200,
925
+ collapsedPaths: inspection.entries.filter((entry) => entry.children > 0 && (compact || entry.depth > 0 && (display2?.expandDepth === void 0 ? entry.children >= 4 : entry.depth >= boundedJsonOption(display2.expandDepth, 1, 0, 8)))).map((entry) => entry.path),
926
+ expandedBuckets: [],
927
+ searchMode: "filter",
928
+ searchOverrides: []
929
+ };
884
930
  }
885
- function JsonValueView({ value, label = "JSON", display: display2, compact = false }) {
931
+ function useInspectionState(props, inspection, compact = false) {
932
+ const key = props.inspectionScope ? jsonInspectionScopeKey(props.inspectionScope) : inspection.raw + JSON.stringify(props.display);
933
+ const [local, setLocal] = useState3(() => ({ key, state: createJsonInspectionState(inspection, props.display, compact) }));
934
+ const state = props.inspectionState ?? (local.key === key ? local.state : createJsonInspectionState(inspection, props.display, compact));
935
+ const change = (next) => {
936
+ if (props.inspectionState === void 0) setLocal({ key, state: next });
937
+ props.onInspectionStateChange?.(next);
938
+ };
939
+ return [state, change];
940
+ }
941
+ function JsonValueView(props) {
942
+ const { value, label = "JSON", display: display2, compact = false } = props;
886
943
  const inspection = useMemo(() => inspectJsonValue(value, display2), [value, display2]);
944
+ const [state, change] = useInspectionState(props, inspection, compact);
887
945
  if (value == null || value === "") return /* @__PURE__ */ jsx3("span", { className: "qt-muted", children: "\u2014" });
888
- return /* @__PURE__ */ jsx3("div", { className: `qt-json-inline${compact ? " qt-json-inline--compact" : ""}`, "data-qt-json-inline": true, children: /* @__PURE__ */ jsx3(JsonValueContent, { inspection, label, display: display2, compact }, `${inspection.raw}
889
- ${JSON.stringify(display2)}
890
- ${compact}`) });
946
+ return /* @__PURE__ */ jsx3("div", { className: `qt-json-inline${compact ? " qt-json-inline--compact" : ""}`, "data-qt-json-inline": true, children: /* @__PURE__ */ jsx3(JsonValueContent, { ...props, inspection, state, change, label, compact }) });
891
947
  }
892
948
  function highlight(text, search, entry, part, active) {
893
949
  const needle = search.trim().toLowerCase();
@@ -902,22 +958,20 @@ function highlight(text, search, entry, part, active) {
902
958
  parts.push(text.slice(start));
903
959
  return parts;
904
960
  }
905
- function JsonValueContent({ inspection, label, display: display2, onClose, compact = false }) {
906
- const [search, setSearch] = useState3("");
907
- const [raw, setRaw] = useState3(Boolean(inspection.error));
961
+ function JsonValueContent({ inspection, label, onClose, compact = false, state, change, inspectionScope, attention, onInspectionResolution }) {
962
+ const { search, raw, searchMode, visibleEntryLimit: pageSize } = state;
963
+ const collapsed = useMemo(() => new Set(state.collapsedPaths), [state.collapsedPaths]);
964
+ const expandedBuckets = useMemo(() => new Set(state.expandedBuckets), [state.expandedBuckets]);
965
+ const searchOverrides = useMemo(() => new Map(state.searchOverrides.map((entry) => [entry.path, entry.expanded])), [state.searchOverrides]);
908
966
  const [copyStatus, setCopyStatus] = useState3("");
909
- const [pageSize, setPageSize] = useState3(200);
910
- const [collapsed, setCollapsed] = useState3(() => new Set(inspection.entries.filter((entry) => entry.children > 0 && (compact || entry.depth > 0 && (display2?.expandDepth === void 0 ? entry.children >= 4 : entry.depth >= boundedJsonOption(display2.expandDepth, 1, 0, 8)))).map((entry) => entry.path)));
911
- const [expandedBuckets, setExpandedBuckets] = useState3(/* @__PURE__ */ new Set());
912
- const [searchMode, setSearchMode] = useState3("filter");
913
- const [matchIndex, setMatchIndex] = useState3(0);
914
967
  const [navigationRequest, setNavigationRequest] = useState3(0);
915
- const [searchOverrides, setSearchOverrides] = useState3(/* @__PURE__ */ new Map());
916
968
  const contentRef = useRef2(null);
917
969
  const searching = Boolean(search.trim());
918
970
  const matches = useMemo(() => searchJsonEntries(inspection.entries, search), [inspection, search]);
919
971
  const occurrences = useMemo(() => jsonSearchMatches(inspection.entries, search), [inspection, search]);
920
- const activeMatch = occurrences[matchIndex];
972
+ const selectedIndex = state.selectedMatch ? occurrences.findIndex((match) => match.path === state.selectedMatch.path && match.part === state.selectedMatch.part && match.start === state.selectedMatch.start) : 0;
973
+ const matchIndex = Math.max(0, selectedIndex);
974
+ const activeMatch = selectedIndex < 0 ? void 0 : occurrences[matchIndex];
921
975
  const matchedPaths = useMemo(() => new Set(matches.map((entry) => entry.path)), [matches]);
922
976
  const byPath = useMemo(() => new Map(inspection.entries.map((entry) => [entry.path, entry])), [inspection]);
923
977
  const presentation = useMemo(() => {
@@ -949,6 +1003,69 @@ function JsonValueContent({ inspection, label, display: display2, onClose, compa
949
1003
  presentation.expandedBuckets,
950
1004
  searching ? JSON_MAX_ENTRIES * 2 : pageSize
951
1005
  ), [inspection, searching, searchMode, matches, presentation, pageSize]);
1006
+ const missingPaths = [...state.collapsedPaths, ...state.expandedBuckets, ...state.searchOverrides.map((entry) => entry.path)].filter((path) => {
1007
+ if (byPath.has(path)) return false;
1008
+ const split = path.lastIndexOf(":bucket:");
1009
+ const parent = byPath.get(path.slice(0, split));
1010
+ return !(split > 0 && parent?.type === "array" && parent.children > Number(path.slice(split + 8)));
1011
+ });
1012
+ const resolutionKey = JSON.stringify([missingPaths, selectedIndex < 0]);
1013
+ const resolutionCallback = useRef2(onInspectionResolution);
1014
+ resolutionCallback.current = onInspectionResolution;
1015
+ useEffect3(() => {
1016
+ resolutionCallback.current?.({ status: missingPaths.length || selectedIndex < 0 ? "unavailable" : "ready", missingPaths, selectedMatchUnavailable: selectedIndex < 0 });
1017
+ }, [resolutionKey]);
1018
+ const [attentionPath, setAttentionPath] = useState3(null);
1019
+ const pendingReveal = useRef2(null);
1020
+ const latest = useRef2({ state, change, byPath });
1021
+ latest.current = { state, change, byPath };
1022
+ const scopeKey = inspectionScope ? jsonInspectionScopeKey(inspectionScope) : "";
1023
+ useEffect3(() => {
1024
+ if (!attention || !inspectionScope) return;
1025
+ return attention.register(inspectionScope, { reveal: (target, signal) => {
1026
+ const path = target.path ?? "$";
1027
+ const current = latest.current;
1028
+ let entry = current.byPath.get(path);
1029
+ if (!entry || signal.aborted) return false;
1030
+ const folded = new Set(current.state.collapsedPaths), buckets = new Set(current.state.expandedBuckets);
1031
+ while (entry) {
1032
+ folded.delete(entry.path);
1033
+ const parent = entry.parent ? current.byPath.get(entry.parent) : void 0;
1034
+ if (parent?.type === "array" && Number(entry.key) >= JSON_ARRAY_BUCKET_SIZE) buckets.add(jsonBucketId(parent.path, Number(entry.key)));
1035
+ entry = parent;
1036
+ }
1037
+ return new Promise((resolve) => {
1038
+ const abort = () => {
1039
+ if (pendingReveal.current?.signal === signal) pendingReveal.current = null;
1040
+ resolve(false);
1041
+ };
1042
+ const finish = (found) => {
1043
+ signal.removeEventListener("abort", abort);
1044
+ resolve(found);
1045
+ };
1046
+ signal.addEventListener("abort", abort, { once: true });
1047
+ pendingReveal.current?.finish(false);
1048
+ pendingReveal.current = { path, signal, finish };
1049
+ const { selectedMatch: _match, ...next } = current.state;
1050
+ current.change({ ...next, raw: false, search: "", searchOverrides: [], collapsedPaths: [...folded], expandedBuckets: [...buckets], visibleEntryLimit: 1e4 });
1051
+ setAttentionPath(path);
1052
+ setNavigationRequest((request) => request + 1);
1053
+ });
1054
+ } });
1055
+ }, [attention, scopeKey]);
1056
+ useLayoutEffect2(() => {
1057
+ const request = pendingReveal.current;
1058
+ if (!request || request.signal.aborted) return;
1059
+ const element = [...contentRef.current?.querySelectorAll(".qt-json-entry") ?? []].find((node) => node.dataset.jsonPath === request.path);
1060
+ if (!element) return;
1061
+ pendingReveal.current = null;
1062
+ element.scrollIntoView?.({ block: "nearest", behavior: "instant" });
1063
+ request.finish(true);
1064
+ });
1065
+ useEffect3(() => () => {
1066
+ pendingReveal.current?.finish(false);
1067
+ pendingReveal.current = null;
1068
+ }, []);
952
1069
  useEffect3(() => {
953
1070
  if (!searching || raw) return;
954
1071
  const content = contentRef.current;
@@ -957,12 +1074,11 @@ function JsonValueContent({ inspection, label, display: display2, onClose, compa
957
1074
  const bounds = content.getBoundingClientRect();
958
1075
  const target = selected.getBoundingClientRect();
959
1076
  if (target.top < bounds.top || target.bottom > bounds.bottom) content.scrollTop += target.top - bounds.top - (bounds.height - target.height) / 2;
960
- }, [searching, raw, activeMatch, searchMode, navigationRequest]);
1077
+ }, [searching, raw, activeMatch?.path, activeMatch?.part, activeMatch?.start, searchMode, navigationRequest]);
961
1078
  const navigate = (direction) => {
962
1079
  if (!occurrences.length) return;
963
- setMatchIndex((current) => (current + direction + occurrences.length) % occurrences.length);
1080
+ change({ ...state, selectedMatch: occurrences[selectedIndex < 0 ? direction > 0 ? 0 : occurrences.length - 1 : (matchIndex + direction + occurrences.length) % occurrences.length], searchOverrides: [] });
964
1081
  setNavigationRequest((current) => current + 1);
965
- setSearchOverrides(/* @__PURE__ */ new Map());
966
1082
  };
967
1083
  const copy2 = async (text, message) => {
968
1084
  try {
@@ -974,19 +1090,19 @@ function JsonValueContent({ inspection, label, display: display2, onClose, compa
974
1090
  };
975
1091
  const toggle = (path, bucket = false) => {
976
1092
  const expanded = bucket ? presentation.expandedBuckets.has(path) : !presentation.collapsed.has(path);
977
- if (searching) setSearchOverrides((current) => new Map(current).set(path, !expanded));
978
- else (bucket ? setExpandedBuckets : setCollapsed)((current) => {
979
- const next = new Set(current);
1093
+ if (searching) change({ ...state, searchOverrides: [...searchOverrides].filter(([key]) => key !== path).map(([path2, expanded2]) => ({ path: path2, expanded: expanded2 })).concat({ path, expanded: !expanded }) });
1094
+ else {
1095
+ const next = new Set(bucket ? expandedBuckets : collapsed);
980
1096
  if (next.has(path)) next.delete(path);
981
1097
  else next.add(path);
982
- return next;
983
- });
1098
+ change({ ...state, ...bucket ? { expandedBuckets: [...next] } : { collapsedPaths: [...next] } });
1099
+ }
984
1100
  };
985
1101
  return /* @__PURE__ */ jsxs3("section", { className: "qt-json-value-view", "aria-label": `${label} JSON value`, children: [
986
1102
  !compact && /* @__PURE__ */ jsxs3("div", { className: "qt-json-toolbar", children: [
987
1103
  onClose && /* @__PURE__ */ jsx3("strong", { className: "qt-json-title", children: label }),
988
1104
  /* @__PURE__ */ jsxs3("div", { className: "qt-json-actions", children: [
989
- /* @__PURE__ */ jsx3("button", { type: "button", className: "qt-link-btn qt-json-action", "aria-label": raw ? "Show JSON" : "Show original", title: raw ? "Show pretty-printed JSON" : "Show original source", "aria-pressed": raw, disabled: Boolean(inspection.error), onClick: () => setRaw(!raw), children: /* @__PURE__ */ jsx3(Icon, { name: "panelDefinition", size: 14 }) }),
1105
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "qt-link-btn qt-json-action", "aria-label": raw ? "Show JSON" : "Show original", title: raw ? "Show pretty-printed JSON" : "Show original source", "aria-pressed": raw, disabled: Boolean(inspection.error), onClick: () => change({ ...state, raw: !raw }), children: /* @__PURE__ */ jsx3(Icon, { name: "panelDefinition", size: 14 }) }),
990
1106
  /* @__PURE__ */ jsx3("button", { type: "button", className: "qt-link-btn qt-json-action", "aria-label": "Copy original", title: "Copy original source", onClick: () => copy2(inspection.raw, "Original value copied"), children: /* @__PURE__ */ jsx3(Icon, { name: "panelReference", size: 14 }) }),
991
1107
  /* @__PURE__ */ jsx3("button", { type: "button", className: "qt-link-btn qt-json-action", "aria-label": "Copy JSON", title: "Copy JSON object or value", disabled: Boolean(inspection.error), onClick: () => copy2(inspection.formatted, "Formatted JSON copied"), children: /* @__PURE__ */ jsx3(Icon, { name: "copy", size: 14 }) })
992
1108
  ] }),
@@ -1000,12 +1116,11 @@ function JsonValueContent({ inspection, label, display: display2, onClose, compa
1000
1116
  value: searchMode,
1001
1117
  disabled: raw || Boolean(inspection.error),
1002
1118
  onChange: (event) => {
1003
- setSearchMode(event.target.value);
1004
- setSearchOverrides(/* @__PURE__ */ new Map());
1119
+ change({ ...state, searchMode: event.target.value, searchOverrides: [] });
1005
1120
  },
1006
1121
  children: [
1007
1122
  /* @__PURE__ */ jsx3("option", { value: "filter", children: "Filter" }),
1008
- /* @__PURE__ */ jsx3("option", { value: "highlight", children: "Highlight" })
1123
+ /* @__PURE__ */ jsx3("option", { value: "find", children: "Find" })
1009
1124
  ]
1010
1125
  }
1011
1126
  ),
@@ -1019,12 +1134,11 @@ function JsonValueContent({ inspection, label, display: display2, onClose, compa
1019
1134
  title: "Search keys, paths and values",
1020
1135
  disabled: raw || Boolean(inspection.error),
1021
1136
  placeholder: "Search JSON",
1137
+ maxLength: 1e4,
1022
1138
  onChange: (event) => {
1023
- setSearch(event.target.value);
1024
- setMatchIndex(0);
1025
- setSearchOverrides(/* @__PURE__ */ new Map());
1139
+ const { selectedMatch: _selected, ...next } = state;
1140
+ change({ ...next, search: event.target.value, searchOverrides: [], visibleEntryLimit: 200 });
1026
1141
  setCopyStatus("");
1027
- setPageSize(200);
1028
1142
  },
1029
1143
  onKeyDown: (event) => {
1030
1144
  if (event.key === "Enter") {
@@ -1035,7 +1149,7 @@ function JsonValueContent({ inspection, label, display: display2, onClose, compa
1035
1149
  }
1036
1150
  ),
1037
1151
  searching && /* @__PURE__ */ jsxs3("span", { className: "qt-json-search-count", "aria-live": "polite", children: [
1038
- occurrences.length ? matchIndex + 1 : 0,
1152
+ activeMatch ? matchIndex + 1 : 0,
1039
1153
  "/",
1040
1154
  occurrences.length
1041
1155
  ] }),
@@ -1048,6 +1162,7 @@ function JsonValueContent({ inspection, label, display: display2, onClose, compa
1048
1162
  ] })
1049
1163
  ] }),
1050
1164
  /* @__PURE__ */ jsxs3("div", { className: "qt-json-content", ref: contentRef, children: [
1165
+ state.selectedMatch && !activeMatch && /* @__PURE__ */ jsx3("p", { className: "qt-json-notice", role: "status", children: "Selected JSON match is unavailable." }),
1051
1166
  inspection.error && /* @__PURE__ */ jsx3("p", { className: "qt-json-notice", role: "alert", children: inspection.error }),
1052
1167
  inspection.raw.length > JSON_MAX_CHARACTERS && /* @__PURE__ */ jsx3("p", { className: "qt-json-notice", children: "Original text is truncated in this view. Copy original retains the full source." }),
1053
1168
  inspection.limited && /* @__PURE__ */ jsx3("p", { className: "qt-json-notice", children: "Inspection is limited to 5,000 entries and 64 levels. Search covers the inspected entries only; Copy JSON includes the full value." }),
@@ -1075,7 +1190,7 @@ function JsonValueContent({ inspection, label, display: display2, onClose, compa
1075
1190
  return /* @__PURE__ */ jsxs3(
1076
1191
  "div",
1077
1192
  {
1078
- className: `qt-json-line${kind === "value" ? " qt-json-entry" : ""}${searching && kind === "value" && matchedPaths.has(entry.path) ? " qt-json-match" : ""}${searching && kind === "value" && activeMatch?.path === entry.path ? " qt-json-match--active" : ""}`,
1193
+ className: `qt-json-line${kind === "value" && attentionPath === entry.path ? " qt-attention-target" : ""}${kind === "value" ? " qt-json-entry" : ""}${searching && kind === "value" && matchedPaths.has(entry.path) ? " qt-json-match" : ""}${searching && kind === "value" && activeMatch?.path === entry.path ? " qt-json-match--active" : ""}`,
1079
1194
  "data-json-path": entry.path,
1080
1195
  title: entry.path,
1081
1196
  style: { paddingInlineStart: `${Math.min(entry.depth + (kind === "omitted" ? 1 : 0), 32) * 16}px` },
@@ -1110,7 +1225,7 @@ function JsonValueContent({ inspection, label, display: display2, onClose, compa
1110
1225
  );
1111
1226
  }),
1112
1227
  lines.length === 0 && /* @__PURE__ */ jsx3("p", { className: "qt-json-notice", children: "No matching keys or values." }),
1113
- remaining > 0 && /* @__PURE__ */ jsx3("button", { type: "button", className: "qt-btn qt-json-more", onClick: () => setPageSize(pageSize + 200), children: "Show more" })
1228
+ remaining > 0 && /* @__PURE__ */ jsx3("button", { type: "button", className: "qt-btn qt-json-more", onClick: () => change({ ...state, visibleEntryLimit: Math.min(1e4, pageSize + 200) }), children: "Show more" })
1114
1229
  ] })
1115
1230
  ] }),
1116
1231
  !compact && /* @__PURE__ */ jsxs3("footer", { className: "qt-json-footer", children: [
@@ -1374,9 +1489,11 @@ import { encodeQuery, renderItemPageTemplate } from "@pythia-software/query-tabl
1374
1489
  import { usePropertyView } from "@pythia-software/query-table-react";
1375
1490
 
1376
1491
  // src/ObjectProperties.tsx
1377
- import { cloneElement, isValidElement, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState4 } from "react";
1492
+ import { cloneElement, isValidElement, useEffect as useEffect4, useLayoutEffect as useLayoutEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState4 } from "react";
1378
1493
  import {
1379
1494
  EMPTY_QUERY,
1495
+ isPropertyFieldLoaded,
1496
+ jsonInspectionScopeKey as jsonInspectionScopeKey2,
1380
1497
  readFieldValue,
1381
1498
  readObjectReference,
1382
1499
  objectReferenceKey
@@ -1648,14 +1765,52 @@ function ObjectProperties(props) {
1648
1765
  const drag = useRef3(null);
1649
1766
  const [copied, setCopied] = useState4(null);
1650
1767
  const [copyError, setCopyError] = useState4("");
1651
- const [jsonViewer, setJsonViewer] = useState4(null);
1768
+ const [localInspections, setLocalInspections] = useState4([]);
1769
+ const inspections = [...props.jsonInspections ?? [], ...localInspections];
1770
+ const idField = schema.fields.find((field) => field.name === schema.idField);
1771
+ const identity = props.recordId ?? (idField ? readFieldValue(idField, row) : row[schema.idField]);
1772
+ const rowId = typeof identity === "string" || typeof identity === "number" && Number.isFinite(identity) ? identity : void 0;
1773
+ const [anonymousInspection, setAnonymousInspection] = useState4(() => ({ row, states: /* @__PURE__ */ new Map() }));
1774
+ const triggers = useRef3(/* @__PURE__ */ new Map());
1775
+ const latest = useRef3(props);
1776
+ latest.current = props;
1777
+ const renderedJson = /* @__PURE__ */ new Map();
1778
+ const jsonTargets = useRef3(renderedJson);
1779
+ useLayoutEffect3(() => {
1780
+ jsonTargets.current = renderedJson;
1781
+ });
1782
+ useEffect4(() => {
1783
+ if (!props.attention || rowId === void 0) return;
1784
+ return props.attention.register({ dataset: schema.name, rowId }, { reveal: async (target, signal) => {
1785
+ if (signal.aborted) return false;
1786
+ if (target.field !== void 0) {
1787
+ const property = latest.current.view.properties.find((entry) => entry.field === target.field);
1788
+ const field = latest.current.schema.fields.find((entry) => entry.name === target.field && entry.property?.enabled !== false);
1789
+ if (!property || !field || !isPropertyFieldLoaded(field, latest.current.loadedFields)) return false;
1790
+ if (target.path !== void 0) {
1791
+ const json = jsonTargets.current.get(target.field);
1792
+ if (!json || !inspectJsonValue(json.value, json.display).entries.some((entry) => entry.path === target.path)) return false;
1793
+ const provider = await props.attention.whenRegistered({ dataset: schema.name, rowId, field: target.field }, signal);
1794
+ return signal.aborted ? false : provider.reveal(target, signal);
1795
+ }
1796
+ const element = [...surface.current?.querySelectorAll("[data-property-field]") ?? []].find((element2) => element2.dataset.propertyField === target.field);
1797
+ if (!element) return false;
1798
+ element.scrollIntoView?.({ block: "nearest", behavior: "instant" });
1799
+ element.classList.add("qt-attention-target");
1800
+ return true;
1801
+ }
1802
+ surface.current?.scrollIntoView?.({ block: "nearest", behavior: "instant" });
1803
+ return Boolean(surface.current);
1804
+ } });
1805
+ }, [props.attention, schema.name, rowId]);
1806
+ const changeInspection = (entry) => {
1807
+ if (!props.jsonInspections || rowId === void 0) setLocalInspections((current) => [...current.filter((previous) => jsonInspectionScopeKey2(previous) !== jsonInspectionScopeKey2(entry)), entry]);
1808
+ if (rowId !== void 0) props.onJsonInspectionChange?.(entry);
1809
+ };
1652
1810
  useEffect4(() => {
1653
1811
  setCopied(null);
1654
1812
  setCopyError("");
1655
1813
  }, [row]);
1656
- useEffect4(() => {
1657
- setJsonViewer(null);
1658
- }, [row]);
1659
1814
  useEffect4(() => {
1660
1815
  if (!copied) return;
1661
1816
  const timer = setTimeout(() => setCopied(null), 1800);
@@ -1777,7 +1932,7 @@ function ObjectProperties(props) {
1777
1932
  let jsonProps;
1778
1933
  if (!field || field.property?.enabled === false)
1779
1934
  content = /* @__PURE__ */ jsx6("span", { className: "qt-property-unavailable", children: "Field unavailable" });
1780
- else if (coverage && !coverage.has(field.name) && !(field.source.kind === "derived" && field.source.dependencies?.every((name) => coverage.has(name))))
1935
+ else if (!isPropertyFieldLoaded(field, loadedFields))
1781
1936
  content = /* @__PURE__ */ jsx6("span", { className: "qt-property-unavailable", children: "Not loaded" });
1782
1937
  else {
1783
1938
  const value = readFieldValue(field, row);
@@ -1788,8 +1943,23 @@ function ObjectProperties(props) {
1788
1943
  const href = result?.href ? safeLinkHref(result.href) : null;
1789
1944
  let display2 = result?.label ?? propertyContent(field, property, value, props, now);
1790
1945
  if (isValidElement(display2) && display2.type === JsonValueView) {
1791
- if (value != null && value !== "") jsonProps = { ...display2.props, label: property.label ?? field.label };
1792
- display2 = cloneElement(display2, { compact: true });
1946
+ if (display2.props.value != null && display2.props.value !== "") {
1947
+ const scope = rowId === void 0 ? void 0 : { dataset: schema.name, rowId, field: property.field };
1948
+ const state = scope ? inspections.find((entry) => jsonInspectionScopeKey2(entry) === jsonInspectionScopeKey2(scope))?.state : anonymousInspection.row === row ? anonymousInspection.states.get(property.field) : void 0;
1949
+ jsonProps = {
1950
+ ...display2.props,
1951
+ label: property.label ?? field.label,
1952
+ inspectionState: state ?? createJsonInspectionState(inspectJsonValue(display2.props.value, display2.props.display), display2.props.display, true),
1953
+ inspectionScope: scope,
1954
+ onInspectionStateChange: (state2) => {
1955
+ if (scope) changeInspection({ ...scope, state: state2 });
1956
+ else setAnonymousInspection((previous) => ({ row, states: new Map(previous.row === row ? previous.states : []).set(property.field, state2) }));
1957
+ },
1958
+ attention: props.attention
1959
+ };
1960
+ renderedJson.set(property.field, jsonProps);
1961
+ }
1962
+ display2 = cloneElement(display2, { ...jsonProps, compact: true });
1793
1963
  }
1794
1964
  content = href ? /* @__PURE__ */ jsxs4(
1795
1965
  "a",
@@ -1820,7 +1990,7 @@ function ObjectProperties(props) {
1820
1990
  }
1821
1991
  ) : display2;
1822
1992
  }
1823
- return /* @__PURE__ */ jsxs4("div", { className: "qt-property", "data-property-type": field?.type, children: [
1993
+ return /* @__PURE__ */ jsxs4("div", { className: "qt-property", "data-property-field": property.field, "data-property-type": field?.type, children: [
1824
1994
  /* @__PURE__ */ jsxs4("dt", { children: [
1825
1995
  /* @__PURE__ */ jsx6("span", { children: property.label ?? field?.label ?? property.field }),
1826
1996
  jsonProps && /* @__PURE__ */ jsx6(
@@ -1830,7 +2000,10 @@ function ObjectProperties(props) {
1830
2000
  className: "qt-link-btn qt-property-json-open",
1831
2001
  "aria-label": `Open ${property.label ?? field?.label ?? property.field} in JSON Viewer`,
1832
2002
  title: "Open in JSON Viewer",
1833
- onClick: (event) => setJsonViewer({ props: jsonProps, trigger: event.currentTarget }),
2003
+ onClick: (event) => {
2004
+ triggers.current.set(property.field, event.currentTarget);
2005
+ jsonProps.onInspectionStateChange?.({ ...jsonProps.inspectionState, open: true });
2006
+ },
1834
2007
  children: /* @__PURE__ */ jsx6(Icon, { name: "panelDefinition", size: 14 })
1835
2008
  }
1836
2009
  )
@@ -1859,10 +2032,18 @@ function ObjectProperties(props) {
1859
2032
  ] })
1860
2033
  ]
1861
2034
  }
2035
+ ),
2036
+ jsonProps?.inspectionState?.open && entries.find((entry) => entry.property.field === property.field)?.property.id === property.id && /* @__PURE__ */ jsx6(
2037
+ JsonViewerModal,
2038
+ {
2039
+ ...jsonProps,
2040
+ returnFocus: triggers.current.get(property.field),
2041
+ preserveFocus: props.preserveFocus && !triggers.current.get(property.field)?.isConnected,
2042
+ onClose: () => jsonProps.onInspectionStateChange?.({ ...jsonProps.inspectionState, open: false })
2043
+ }
1862
2044
  )
1863
- ] }, property.id);
2045
+ ] }, JSON.stringify([rowId, property.id]));
1864
2046
  }) }),
1865
- jsonViewer && /* @__PURE__ */ jsx6(JsonViewerModal, { ...jsonViewer.props, onClose: () => setJsonViewer(null), returnFocus: jsonViewer.trigger }),
1866
2047
  copyError && /* @__PURE__ */ jsx6("p", { className: "qt-property-copy-error", role: "status", children: copyError }),
1867
2048
  resolved.error ? /* @__PURE__ */ jsxs4("p", { role: "status", className: "qt-property-unavailable", children: [
1868
2049
  "Relationship labels unavailable: ",
@@ -1884,7 +2065,7 @@ import { useMemo as useMemo3, useState as useState7, useEffect as useEffect6, us
1884
2065
  import {
1885
2066
  createContext,
1886
2067
  useContext,
1887
- useLayoutEffect as useLayoutEffect2,
2068
+ useLayoutEffect as useLayoutEffect4,
1888
2069
  useRef as useRef4,
1889
2070
  useState as useState5
1890
2071
  } from "react";
@@ -1907,7 +2088,7 @@ function PickerSurface({
1907
2088
  const returnFocus = useRef4(null);
1908
2089
  close.current = onClose;
1909
2090
  const [position, setPosition] = useState5({ top: 0, left: 0, maxHeight: 440 });
1910
- useLayoutEffect2(() => {
2091
+ useLayoutEffect4(() => {
1911
2092
  if (mobile) return;
1912
2093
  const element = surface.current;
1913
2094
  if (!element) return;
@@ -2614,7 +2795,7 @@ function useMetricResize() {
2614
2795
  }
2615
2796
 
2616
2797
  // src/useEditorWorkbench.ts
2617
- import { useLayoutEffect as useLayoutEffect3, useRef as useRef8, useState as useState8 } from "react";
2798
+ import { useLayoutEffect as useLayoutEffect5, useRef as useRef8, useState as useState8 } from "react";
2618
2799
  function fitPanes(previous, available, visible, minimums) {
2619
2800
  const total = Math.max(
2620
2801
  minimums.reduce((sum, width, i) => sum + (visible[i] ? width : 0), 0),
@@ -2670,7 +2851,7 @@ function useEditorWorkbench(panes5, isEditor = true) {
2670
2851
  (visible, i) => visible ? [i] : []
2671
2852
  );
2672
2853
  const widths = fitPanes(preferredWidths, available, visiblePanes, minimums);
2673
- useLayoutEffect3(() => {
2854
+ useLayoutEffect5(() => {
2674
2855
  const element = root.current;
2675
2856
  if (!element || !isEditor) return;
2676
2857
  const fit = () => {
@@ -3752,6 +3933,11 @@ function DetailContent({ api, record, loading, error, onRetry, renderers, mode,
3752
3933
  {
3753
3934
  schema: api.schema,
3754
3935
  row: record.row,
3936
+ recordId: id,
3937
+ ...api.jsonInspections ? { jsonInspections: api.jsonInspections } : {},
3938
+ ...api.setJsonInspection ? { onJsonInspectionChange: api.setJsonInspection } : {},
3939
+ attention: api.attention,
3940
+ preserveFocus: api.preserveFocus,
3755
3941
  ...api.subject === "group" ? { query } : {},
3756
3942
  propertyApi: api.propertiesByMode[mode],
3757
3943
  viewScope: mode === "hover" ? "Popover" : mode === "expand" ? "Row expand" : "Modal",
@@ -3806,7 +3992,7 @@ function ItemDetailsPanel({ api, query, renderers, outsidePage = false }) {
3806
3992
  if (!api.active) return null;
3807
3993
  const { id, mode } = api.active;
3808
3994
  const content = /* @__PURE__ */ jsx17(DetailContent, { api, record: api.record, loading: api.loading, error: api.error, onRetry: api.retry, renderers, mode, query, id, onClose: api.close });
3809
- if (mode === "modal") return /* @__PURE__ */ jsx17(ModalSurface, { title: api.subject === "group" ? "Group details" : "Item details", className: "qt-modal qt-item-detail-modal", chrome: false, onClose: api.close, children: content });
3995
+ if (mode === "modal") return /* @__PURE__ */ jsx17(ModalSurface, { title: api.subject === "group" ? "Group details" : "Item details", className: "qt-modal qt-item-detail-modal", preserveFocus: api.preserveFocus, chrome: false, onClose: api.close, children: content });
3810
3996
  const noun = api.subject === "group" ? "group" : "item";
3811
3997
  return /* @__PURE__ */ jsxs13("section", { className: "qt-item-expanded", "aria-label": `Expanded ${noun} ${id}`, children: [
3812
3998
  outsidePage && /* @__PURE__ */ jsxs13("p", { className: "qt-item-detail-note", children: [
@@ -4017,8 +4203,8 @@ function ItemHoverCard({ api, query, id, renderers, anchor, onClose }) {
4017
4203
 
4018
4204
  // src/DataTable.tsx
4019
4205
  import { GROUP_ID_FIELD, GROUP_LINK_RENDER, ITEM_DETAILS_COLUMN, groupKeyFields, isComputedCellError, isGroupLinkValue as isGroupLinkValue2 } from "@pythia-software/query-table-core";
4020
- import { useId as useId4, useCallback as useCallback3, useEffect as useEffect12, useLayoutEffect as useLayoutEffect4, useMemo as useMemo5, useRef as useRef14, useState as useState16 } from "react";
4021
- import { useVirtualizer } from "@tanstack/react-virtual";
4206
+ import { useId as useId4, useCallback as useCallback3, useEffect as useEffect12, useLayoutEffect as useLayoutEffect6, useMemo as useMemo5, useRef as useRef14, useState as useState16 } from "react";
4207
+ import { defaultRangeExtractor, useVirtualizer } from "@tanstack/react-virtual";
4022
4208
  import { isSortable, readFieldValue as readFieldValue2 } from "@pythia-software/query-table-core";
4023
4209
  import { useColumnDrag } from "@pythia-software/query-table-react";
4024
4210
 
@@ -4769,8 +4955,14 @@ function DataTable(props) {
4769
4955
  const estimateVirtualRow = useCallback3((index) => entries[index]?.detail ? 400 : normalizedEstimate, [normalizedEstimate, entries]);
4770
4956
  const initialViewportHeight = typeof maxHeight === "number" && maxHeight > 0 ? maxHeight : DEFAULT_MAX_HEIGHT;
4771
4957
  const initialRect = useMemo5(() => ({ width: 0, height: initialViewportHeight }), [initialViewportHeight]);
4958
+ const [attentionRange, setAttentionRange] = useState16(null);
4959
+ const holdingAttention = attentionRange?.index === expandedRowIndex && attentionRange?.revision === details?.attentionRevision;
4960
+ const detailRange = useCallback3((range) => {
4961
+ return defaultRangeExtractor((details?.attentionPending || holdingAttention) && expandedRowIndex >= 0 ? { ...range, startIndex: expandedRowIndex, endIndex: expandedRowIndex + 1 } : range);
4962
+ }, [details?.attentionPending, holdingAttention, expandedRowIndex]);
4772
4963
  const rowVirtualizer = useVirtualizer({
4773
4964
  count: entries.length,
4965
+ rangeExtractor: detailRange,
4774
4966
  getScrollElement: () => tableWrapRef.current,
4775
4967
  estimateSize: estimateVirtualRow,
4776
4968
  getItemKey: getVirtualRowKey,
@@ -4782,9 +4974,17 @@ function DataTable(props) {
4782
4974
  // Row refs measure during React's commit phase; schedule size updates normally.
4783
4975
  useFlushSync: false
4784
4976
  });
4977
+ useLayoutEffect6(() => {
4978
+ if (details?.attentionPending && expandedRowIndex >= 0) {
4979
+ setAttentionRange((previous) => previous?.index === expandedRowIndex && previous.revision === details.attentionRevision ? previous : { index: expandedRowIndex, revision: details.attentionRevision });
4980
+ } else {
4981
+ const range = rowVirtualizer.range;
4982
+ if (expandedRowIndex < 0 || range && range.startIndex - normalizedOverscan <= expandedRowIndex + 1 && range.endIndex + normalizedOverscan >= expandedRowIndex + 1) setAttentionRange(null);
4983
+ }
4984
+ }, [details?.attentionPending, details?.attentionRevision, expandedRowIndex, rowVirtualizer.range?.startIndex, rowVirtualizer.range?.endIndex, normalizedOverscan]);
4785
4985
  useEffect12(() => {
4786
4986
  if (expandedRowIndex >= 0) rowVirtualizer.scrollToIndex(expandedRowIndex, { align: "start" });
4787
- }, [expandedRowIndex]);
4987
+ }, [expandedRowIndex, details?.attentionRevision]);
4788
4988
  useEffect12(() => {
4789
4989
  return () => {
4790
4990
  if (copiedCellTimerRef.current) {
@@ -5085,7 +5285,7 @@ function DataTable(props) {
5085
5285
  const renderedColumnNames = useMemo5(() => columnDrag.preview(tableColumnNames), [columnDrag, tableColumnNames]);
5086
5286
  const dragPreviewRef = useRef14(null);
5087
5287
  const dropRevisionRef = useRef14(columnDrag.dropRevision);
5088
- const useCommitLayoutEffect = typeof window === "undefined" ? useEffect12 : useLayoutEffect4;
5288
+ const useCommitLayoutEffect = typeof window === "undefined" ? useEffect12 : useLayoutEffect6;
5089
5289
  useCommitLayoutEffect(() => {
5090
5290
  if (columnDrag.active) {
5091
5291
  dragPreviewRef.current = renderedColumnNames;
@@ -5233,6 +5433,7 @@ function DataTable(props) {
5233
5433
  onWheel: onTableWheel,
5234
5434
  style: { maxHeight, overflowY: "auto" },
5235
5435
  onScroll: () => {
5436
+ if (!details?.attentionPending) setAttentionRange(null);
5236
5437
  const wrap = tableWrapRef.current;
5237
5438
  if (!wrap) return;
5238
5439
  setShowHorizontalCue(wrap.scrollWidth - wrap.clientWidth - wrap.scrollLeft > 2);
@@ -6160,7 +6361,7 @@ function ColumnPicker({
6160
6361
  // src/grouping/GroupedColumnsEditor.tsx
6161
6362
  import {
6162
6363
  useEffect as useEffect14,
6163
- useLayoutEffect as useLayoutEffect5,
6364
+ useLayoutEffect as useLayoutEffect7,
6164
6365
  useId as useId6,
6165
6366
  useMemo as useMemo7,
6166
6367
  useRef as useRef16,
@@ -7238,7 +7439,7 @@ function GroupedColumnsEditor({
7238
7439
  const workbench = useEditorWorkbench(panes2.map(
7239
7440
  (pane, index) => index === 2 ? { ...pane, defaultVisible: editingAggregate } : pane
7240
7441
  ));
7241
- useLayoutEffect5(() => {
7442
+ useLayoutEffect7(() => {
7242
7443
  if (editingAggregate) workbench.showPane(2);
7243
7444
  else {
7244
7445
  if (workbench.visibleIndices.length === 1 && workbench.visiblePanes[2])
@@ -8621,7 +8822,7 @@ import {
8621
8822
  Suspense as Suspense2,
8622
8823
  useEffect as useEffect17,
8623
8824
  useId as useId8,
8624
- useLayoutEffect as useLayoutEffect6,
8825
+ useLayoutEffect as useLayoutEffect8,
8625
8826
  useMemo as useMemo11,
8626
8827
  useRef as useRef18,
8627
8828
  useState as useState27
@@ -10625,7 +10826,7 @@ function MetricsEditor({
10625
10826
  retry,
10626
10827
  api.aggregations.preview
10627
10828
  ]);
10628
- useLayoutEffect6(() => {
10829
+ useLayoutEffect8(() => {
10629
10830
  setInspectOpen(false);
10630
10831
  setInspectedKey(null);
10631
10832
  pendingInsertion.current = null;
@@ -10657,7 +10858,7 @@ function MetricsEditor({
10657
10858
  observer.observe(element);
10658
10859
  return () => observer.disconnect();
10659
10860
  }, [view, !!selected]);
10660
- useLayoutEffect6(() => {
10861
+ useLayoutEffect8(() => {
10661
10862
  if (view === "dashboard") {
10662
10863
  const card2 = Array.from(
10663
10864
  canvasElement.current?.querySelectorAll(
@@ -16645,6 +16846,7 @@ export {
16645
16846
  SelectColumnEditor,
16646
16847
  SelectionToolbar,
16647
16848
  createGroupLinkRenderer,
16849
+ createJsonInspectionState,
16648
16850
  createMetricColorResolver,
16649
16851
  defaultRenderers,
16650
16852
  formatMetricOutput,