@kanzo-tech/graph 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +25 -7
  2. package/dist/core/detail.d.ts +3 -1
  3. package/dist/core/detail.d.ts.map +1 -1
  4. package/dist/core/detail.js +55 -30
  5. package/dist/core/detail.js.map +1 -1
  6. package/dist/core/error.d.ts +16 -0
  7. package/dist/core/error.d.ts.map +1 -0
  8. package/dist/core/error.js +14 -0
  9. package/dist/core/error.js.map +1 -0
  10. package/dist/core/filter.d.ts +0 -13
  11. package/dist/core/filter.d.ts.map +1 -1
  12. package/dist/core/filter.js +47 -55
  13. package/dist/core/filter.js.map +1 -1
  14. package/dist/core/load.d.ts +5 -0
  15. package/dist/core/load.d.ts.map +1 -1
  16. package/dist/core/load.js +93 -78
  17. package/dist/core/load.js.map +1 -1
  18. package/dist/core/state.d.ts +21 -6
  19. package/dist/core/state.d.ts.map +1 -1
  20. package/dist/core/store.d.ts.map +1 -1
  21. package/dist/core/store.js +107 -101
  22. package/dist/core/store.js.map +1 -1
  23. package/dist/core/types.d.ts +4 -4
  24. package/dist/core/types.d.ts.map +1 -1
  25. package/dist/index.d.ts +8 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +27 -19
  28. package/dist/index.js.map +1 -1
  29. package/dist/parts/graph-canvas.js +45 -45
  30. package/dist/parts/graph-canvas.js.map +1 -1
  31. package/dist/parts/graph-counts.d.ts +15 -0
  32. package/dist/parts/graph-counts.d.ts.map +1 -0
  33. package/dist/parts/graph-counts.js +41 -0
  34. package/dist/parts/graph-counts.js.map +1 -0
  35. package/dist/parts/graph-inspector.d.ts.map +1 -1
  36. package/dist/parts/graph-inspector.js +48 -48
  37. package/dist/parts/graph-inspector.js.map +1 -1
  38. package/dist/parts/graph-legend.d.ts +3 -2
  39. package/dist/parts/graph-legend.d.ts.map +1 -1
  40. package/dist/parts/graph-legend.js +30 -48
  41. package/dist/parts/graph-legend.js.map +1 -1
  42. package/dist/parts/graph-looks.d.ts +16 -0
  43. package/dist/parts/graph-looks.d.ts.map +1 -0
  44. package/dist/parts/graph-looks.js +138 -0
  45. package/dist/parts/graph-looks.js.map +1 -0
  46. package/dist/parts/graph-search.d.ts +17 -0
  47. package/dist/parts/graph-search.d.ts.map +1 -0
  48. package/dist/parts/graph-search.js +89 -0
  49. package/dist/parts/graph-search.js.map +1 -0
  50. package/dist/react/use-graph-prefs.d.ts +6 -2
  51. package/dist/react/use-graph-prefs.d.ts.map +1 -1
  52. package/dist/react/use-graph-prefs.js +7 -7
  53. package/dist/react/use-graph-prefs.js.map +1 -1
  54. package/dist/react/use-graph.js +61 -55
  55. package/dist/react/use-graph.js.map +1 -1
  56. package/dist/render/camera.d.ts +26 -0
  57. package/dist/render/camera.d.ts.map +1 -0
  58. package/dist/render/camera.js +40 -0
  59. package/dist/render/camera.js.map +1 -0
  60. package/dist/render/graph-looks.d.ts +10 -0
  61. package/dist/render/graph-looks.d.ts.map +1 -1
  62. package/dist/render/graph-looks.js +37 -20
  63. package/dist/render/graph-looks.js.map +1 -1
  64. package/dist/render/placement.d.ts +23 -0
  65. package/dist/render/placement.d.ts.map +1 -0
  66. package/dist/render/placement.js +23 -0
  67. package/dist/render/placement.js.map +1 -0
  68. package/dist/render/renderer.d.ts +1 -0
  69. package/dist/render/renderer.d.ts.map +1 -1
  70. package/dist/render/renderer.js +115 -96
  71. package/dist/render/renderer.js.map +1 -1
  72. package/dist/render/webgl.d.ts +5 -4
  73. package/dist/render/webgl.d.ts.map +1 -1
  74. package/dist/render/webgl.js +14 -11
  75. package/dist/render/webgl.js.map +1 -1
  76. package/package.json +10 -9
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import { jsxs as b, jsx as a } from "react/jsx-runtime";
3
- import { useThemeTick as Y, categoricalCapacity as Z, Show as g, cn as ee } from "@kanzo-tech/ui";
3
+ import { useThemeTick as Y, categoricalCapacity as Z, Show as f, cn as ee } from "@kanzo-tech/ui";
4
4
  import { useMemo as x, useCallback as $, useRef as te, useEffect as y, useState as oe } from "react";
5
5
  import { nameOf as ne } from "../core/categories.js";
6
6
  import { readTitles as re } from "../core/detail.js";
7
- import { useGraphContext as F } from "../react/graph-root.js";
7
+ import { useGraphContext as D } from "../react/graph-root.js";
8
8
  import { internalsOf as ae } from "../react/use-graph.js";
9
9
  import { useGraphState as m, useGraphSnapshot as S } from "../react/use-graph-state.js";
10
10
  import { resolveLook as se } from "../render/graph-looks.js";
@@ -12,28 +12,28 @@ import { scaleOf as le } from "../render/graph-model.js";
12
12
  import { useGesture as ie, cursorChip as ce } from "./gesture.js";
13
13
  import { useOverlays as pe } from "./overlays.js";
14
14
  import { ShapeGlyph as de } from "./shape-glyph.js";
15
- const E = "color-mix(in oklab, var(--primary) 17%, transparent)";
15
+ const F = "color-mix(in oklab, var(--primary) 17%, transparent)";
16
16
  function me(u, d, i, p, s) {
17
- const c = d.sizes, n = [];
17
+ const c = d.sizes, o = [];
18
18
  if (c && p > 0)
19
19
  for (let t = 0; t < u.size; t++) {
20
- const r = c[t];
21
- if (i && !i[t] || Number.isNaN(r) || n.length === p && r <= c[n[p - 1]]) continue;
22
- let o = n.length;
23
- for (; o > 0 && c[n[o - 1]] < r; ) o--;
24
- n.splice(o, 0, t), n.length > p && n.pop();
20
+ const n = c[t];
21
+ if (i && !i[t] || Number.isNaN(n) || o.length === p && n <= c[o[p - 1]]) continue;
22
+ let r = o.length;
23
+ for (; r > 0 && c[o[r - 1]] < n; ) r--;
24
+ o.splice(r, 0, t), o.length > p && o.pop();
25
25
  }
26
- return s === null || n.includes(s) ? n : [s, ...n];
26
+ return s === null || o.includes(s) ? o : [s, ...o];
27
27
  }
28
- function D(u) {
29
- const d = F(), i = m((r) => r.corpus), p = m((r) => r.options.title), s = S((r) => r.geometry), [c, n] = oe(() => /* @__PURE__ */ new Map()), t = u.join(",");
28
+ function E(u) {
29
+ const d = D(), i = m((n) => n.corpus), p = m((n) => n.options.title), s = S((n) => n.geometry), [c, o] = oe(() => /* @__PURE__ */ new Map()), t = u.join(",");
30
30
  return y(() => {
31
31
  if (!i || !s || t === "") return;
32
- const r = new AbortController();
33
- return re(i, s, t.split(",").map(Number), p, r.signal).then(
34
- (o) => !r.signal.aborted && n(o),
35
- (o) => !r.signal.aborted && d.getState().options.onFailure(o instanceof Error ? o.message : String(o))
36
- ), () => r.abort();
32
+ const n = new AbortController();
33
+ return re(i, s, t.split(",").map(Number), p, n.signal).then(
34
+ (r) => !n.signal.aborted && o(r),
35
+ (r) => !n.signal.aborted && d.getState().options.onFailure(r)
36
+ ), () => n.abort();
37
37
  }, [d, i, s, t, p]), c;
38
38
  }
39
39
  const ue = {
@@ -41,13 +41,13 @@ const ue = {
41
41
  loading: "Loading the graph…"
42
42
  };
43
43
  function Me({ children: u, className: d, slot: i, ...p }) {
44
- const s = F(), { attach: c, renderer: n } = ae(s), t = m((e) => e.focus), r = m((e) => e.tool), o = m((e) => e.options), k = m((e) => {
44
+ const s = D(), { attach: c, renderer: o } = ae(s), t = m((e) => e.focus), n = m((e) => e.tool), r = m((e) => e.options), k = m((e) => {
45
45
  var h;
46
46
  return ((h = e.drawn) == null ? void 0 : h.domain) ?? e.domain;
47
- }), w = m((e) => e.drawn === null ? ue[e.status] : void 0), f = x(() => se(o.look), [o.look]), R = $(() => {
47
+ }), w = m((e) => e.drawn === null ? ue[e.status] : void 0), g = x(() => se(r.look), [r.look]), R = $(() => {
48
48
  var e;
49
- return ((e = n()) == null ? void 0 : e.graph) ?? null;
50
- }, [n]), q = pe({ getGraph: R }), { cardRef: B, gridRef: J, hostRef: T, hoverAt: _, labelRef: K, schedule: v, setHovered: P, setLabelOrder: j, track: A } = q, I = te(null), G = S((e) => e.geometry), N = S((e) => e.encoding), W = S((e) => e.mask);
49
+ return ((e = o()) == null ? void 0 : e.graph) ?? null;
50
+ }, [o]), q = pe({ getGraph: R }), { cardRef: B, gridRef: J, hostRef: T, hoverAt: _, labelRef: K, schedule: v, setHovered: P, setLabelOrder: j, track: A } = q, I = te(null), G = S((e) => e.geometry), N = S((e) => e.encoding), W = S((e) => e.mask);
51
51
  y(() => {
52
52
  const e = I.current;
53
53
  if (e)
@@ -56,25 +56,25 @@ function Me({ children: u, className: d, slot: i, ...p }) {
56
56
  const Q = Y();
57
57
  y(() => {
58
58
  var e;
59
- (e = n()) == null || e.repaint();
60
- }, [n, Q]);
59
+ (e = o()) == null || e.repaint();
60
+ }, [o, Q]);
61
61
  const C = x(
62
- () => G && N ? me(G, N, W, f.labels, t) : [],
63
- [G, N, W, t, f.labels]
64
- ), M = D(C), z = x(
62
+ () => G && N ? me(G, N, W, g.labels, t) : [],
63
+ [G, N, W, t, g.labels]
64
+ ), M = E(C), z = x(
65
65
  () => C.flatMap((e) => M.get(e) ? [{ vertex: e, text: M.get(e) }] : []),
66
66
  [C, M]
67
67
  );
68
68
  y(() => {
69
69
  j(z.map((e) => e.vertex)), A(), v();
70
- }, [z, v, j, A, f.grid]);
70
+ }, [z, v, j, A, g.grid]);
71
71
  const H = ie({
72
72
  getGraph: R,
73
73
  getSelection: $(() => s.getState().selection, [s]),
74
74
  commit: (e, h, X) => s.select(e ? [...e] : null, h, X),
75
75
  setTool: s.setTool,
76
- tool: r
77
- }), { active: U, drag: l, preview: O } = H, L = T.current ? Z(T.current) : void 0, V = x(() => le(o, L), [o, L]);
76
+ tool: n
77
+ }), { active: U, drag: l, preview: O } = H, L = T.current ? Z(T.current) : void 0, V = x(() => le(r, L), [r, L]);
78
78
  return /* @__PURE__ */ b(
79
79
  "div",
80
80
  {
@@ -84,7 +84,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
84
84
  ref: T,
85
85
  children: [
86
86
  /* @__PURE__ */ a("div", { className: "size-full", "data-slot": "graph-canvas-surface", ref: I }),
87
- /* @__PURE__ */ a(g, { when: f.grid, children: /* @__PURE__ */ a(
87
+ /* @__PURE__ */ a(f, { when: g.grid, children: /* @__PURE__ */ a(
88
88
  "div",
89
89
  {
90
90
  className: "pointer-events-none absolute inset-0",
@@ -92,7 +92,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
92
92
  style: { backgroundImage: "radial-gradient(var(--border) 1px, transparent 1px)" }
93
93
  }
94
94
  ) }),
95
- /* @__PURE__ */ a(g, { when: f.vignette, children: /* @__PURE__ */ a(
95
+ /* @__PURE__ */ a(f, { when: g.vignette, children: /* @__PURE__ */ a(
96
96
  "div",
97
97
  {
98
98
  className: "pointer-events-none absolute inset-0",
@@ -115,14 +115,14 @@ function Me({ children: u, className: d, slot: i, ...p }) {
115
115
  cardRef: B,
116
116
  domain: k,
117
117
  encoding: N,
118
- options: o,
118
+ options: r,
119
119
  scale: V,
120
120
  schedule: v,
121
121
  setHovered: P
122
122
  }
123
123
  )
124
124
  ] }),
125
- /* @__PURE__ */ a(g, { when: U !== null, children: /* @__PURE__ */ b("div", { className: "absolute inset-0 cursor-crosshair", "data-slot": "graph-canvas-gesture", ...H.handlers, children: [
125
+ /* @__PURE__ */ a(f, { when: U !== null, children: /* @__PURE__ */ b("div", { className: "absolute inset-0 cursor-crosshair", "data-slot": "graph-canvas-gesture", ...H.handlers, children: [
126
126
  l && /* @__PURE__ */ a("svg", { className: "pointer-events-none size-full", children: l.tool === "rect" ? /* @__PURE__ */ a(
127
127
  "rect",
128
128
  {
@@ -130,7 +130,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
130
130
  height: Math.abs(l.to[1] - l.from[1]),
131
131
  strokeDasharray: "4 3",
132
132
  strokeWidth: 1,
133
- style: { fill: E },
133
+ style: { fill: F },
134
134
  width: Math.abs(l.to[0] - l.from[0]),
135
135
  x: Math.min(l.from[0], l.to[0]),
136
136
  y: Math.min(l.from[1], l.to[1])
@@ -141,10 +141,10 @@ function Me({ children: u, className: d, slot: i, ...p }) {
141
141
  className: "stroke-primary",
142
142
  points: l.path.map(([e, h]) => `${e},${h}`).join(" "),
143
143
  strokeWidth: 1,
144
- style: { fill: E }
144
+ style: { fill: F }
145
145
  }
146
146
  ) }),
147
- /* @__PURE__ */ a(g, { when: O !== null, children: /* @__PURE__ */ b(
147
+ /* @__PURE__ */ a(f, { when: O !== null, children: /* @__PURE__ */ b(
148
148
  "span",
149
149
  {
150
150
  className: "pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums",
@@ -157,7 +157,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
157
157
  }
158
158
  ) })
159
159
  ] }) }),
160
- /* @__PURE__ */ a(g, { when: w !== void 0, children: /* @__PURE__ */ a(
160
+ /* @__PURE__ */ a(f, { when: w !== void 0, children: /* @__PURE__ */ a(
161
161
  "p",
162
162
  {
163
163
  className: "pointer-events-none absolute inset-0 grid place-items-center text-muted-foreground text-xs",
@@ -170,12 +170,12 @@ function Me({ children: u, className: d, slot: i, ...p }) {
170
170
  }
171
171
  );
172
172
  }
173
- function he({ cardRef: u, domain: d, encoding: i, options: p, scale: s, schedule: c, setHovered: n }) {
174
- const t = m((w) => w.hovered), r = D(x(() => t === null ? [] : [t], [t]));
173
+ function he({ cardRef: u, domain: d, encoding: i, options: p, scale: s, schedule: c, setHovered: o }) {
174
+ const t = m((w) => w.hovered), n = E(x(() => t === null ? [] : [t], [t]));
175
175
  if (y(() => {
176
- n(t), c();
177
- }, [t, c, n, r]), t === null || !i || t >= i.ranks.length) return null;
178
- const o = i.ranks[t] ?? 0, k = o < d.length ? ne(d[o], p.categories) : null;
176
+ o(t), c();
177
+ }, [t, c, o, n]), t === null || !i || t >= i.ranks.length) return null;
178
+ const r = i.ranks[t] ?? 0, k = r < d.length ? ne(d[r], p.categories) : null;
179
179
  return /* @__PURE__ */ b(
180
180
  "div",
181
181
  {
@@ -183,9 +183,9 @@ function he({ cardRef: u, domain: d, encoding: i, options: p, scale: s, schedule
183
183
  "data-slot": "graph-canvas-card",
184
184
  ref: u,
185
185
  children: [
186
- /* @__PURE__ */ a(de, { className: "size-2.5 shrink-0", color: s.color(o), shape: s.shape(o) }),
187
- /* @__PURE__ */ a("span", { className: "truncate font-medium text-popover-foreground text-sm leading-none", children: r.get(t) || `#${t}` }),
188
- /* @__PURE__ */ a(g, { when: k !== null, children: /* @__PURE__ */ a("span", { className: "text-muted-foreground text-xs", children: k }) })
186
+ /* @__PURE__ */ a(de, { className: "size-2.5 shrink-0", color: s.color(r), shape: s.shape(r) }),
187
+ /* @__PURE__ */ a("span", { className: "truncate font-medium text-popover-foreground text-sm leading-none", children: n.get(t) || `#${t}` }),
188
+ /* @__PURE__ */ a(f, { when: k !== null, children: /* @__PURE__ */ a("span", { className: "text-muted-foreground text-xs", children: k }) })
189
189
  ]
190
190
  }
191
191
  );
@@ -1 +1 @@
1
- {"version":3,"file":"graph-canvas.js","sources":["../../src/parts/graph-canvas.tsx"],"sourcesContent":["\"use client\";\n\nimport { categoricalCapacity, cn, Show, useThemeTick } from \"@kanzo-tech/ui\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { readTitles } from \"../core/detail\";\nimport type { Encoding, Geometry } from \"../core/load\";\nimport type { GraphOptions } from \"../core/state\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { internalsOf } from \"../react/use-graph\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\nimport { resolveLook } from \"../render/graph-looks\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { cursorChip, useGesture } from \"./gesture\";\nimport { useOverlays } from \"./overlays\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphCanvasProps extends React.ComponentProps<\"div\"> {\n /** The chrome — a toolbar, a legend, an inspector — positioned over the surface. */\n children?: ReactNode;\n}\n\nconst WASH = \"color-mix(in oklab, var(--primary) 17%, transparent)\";\n\n/**\n * The biggest `budget` surviving vertices by the ramp, the focused one first. One pass, keeping the\n * best few in order: a sort of every vertex to name twenty-six of them is the cost this avoids.\n */\nfunction labelled(geometry: Geometry, encoding: Encoding, mask: Uint8Array | null, budget: number, focus: VertexId | null): VertexId[] {\n const ramp = encoding.sizes;\n const best: VertexId[] = [];\n if (ramp && budget > 0) {\n for (let id = 0; id < geometry.size; id++) {\n const value = ramp[id] as number;\n if ((mask && !mask[id]) || Number.isNaN(value)) continue;\n if (best.length === budget && value <= (ramp[best[budget - 1] as number] as number)) continue;\n let at = best.length;\n while (at > 0 && (ramp[best[at - 1] as number] as number) < value) at--;\n best.splice(at, 0, id);\n if (best.length > budget) best.pop();\n }\n }\n return focus === null || best.includes(focus) ? best : [focus, ...best];\n}\n\n/** A label's text is read for the vertices that carry one, never carried for every vertex. */\nfunction useTitles(vertices: readonly VertexId[]): ReadonlyMap<VertexId, string> {\n const api = useGraphContext();\n const corpus = useGraphState((s) => s.corpus);\n const title = useGraphState((s) => s.options.title);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const [titles, setTitles] = useState<ReadonlyMap<VertexId, string>>(() => new Map());\n const key = vertices.join(\",\");\n useEffect(() => {\n if (!corpus || !geometry || key === \"\") return;\n const aborter = new AbortController();\n readTitles(corpus, geometry, key.split(\",\").map(Number), title, aborter.signal).then(\n (found) => !aborter.signal.aborted && setTitles(found),\n (error: unknown) => !aborter.signal.aborted && api.getState().options.onFailure(error instanceof Error ? error.message : String(error)),\n );\n return () => aborter.abort();\n }, [api, corpus, geometry, key, title]);\n return titles;\n}\n\nconst WAITING: Partial<Record<string, string>> = {\n opening: \"Opening the corpus…\",\n loading: \"Loading the graph…\",\n};\n\n/**\n * **The element, and what the look declares over it** — the grid, the vignette, the labels and the\n * hover card are fields of `Look`, so drawing them is the canvas's; the marquee and lasso gesture is\n * too. Two layers: cosmos.gl fills the surface with a canvas of its own, so chrome parented there\n * would be a sibling inside an element the renderer resizes. The frame is `isolate`, so a host's\n * `z-index` on a child cannot escape into the page.\n *\n * The surface is never gated on the first answer: rendered behind a placeholder it deadlocks — no\n * element, no renderer, no camera, no question. Waiting is an overlay over an empty canvas.\n */\nexport function GraphCanvas({ children, className, slot, ...rest }: GraphCanvasProps) {\n const api = useGraphContext();\n const { attach, renderer } = internalsOf(api);\n const focus = useGraphState((s) => s.focus);\n const tool = useGraphState((s) => s.tool);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const waiting = useGraphState((s) => (s.drawn === null ? WAITING[s.status] : undefined));\n const look = useMemo(() => resolveLook(options.look), [options.look]);\n const getGraph = useCallback(() => renderer()?.graph ?? null, [renderer]);\n const overlays = useOverlays({ getGraph });\n const { cardRef, gridRef, hostRef, hoverAt, labelRef, schedule, setHovered, setLabelOrder, track } = overlays;\n const surfaceRef = useRef<HTMLDivElement>(null);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const encoding = useGraphSnapshot((s) => s.encoding);\n const mask = useGraphSnapshot((s) => s.mask);\n\n useEffect(() => {\n const surface = surfaceRef.current;\n if (!surface) return;\n return attach(surface, { onFrame: schedule, onHover: hoverAt });\n }, [attach, hoverAt, schedule]);\n\n const themeTick = useThemeTick();\n useEffect(() => {\n renderer()?.repaint();\n }, [renderer, themeTick]);\n\n const labelledIds = useMemo(\n () => (geometry && encoding ? labelled(geometry, encoding, mask, look.labels, focus) : []),\n [geometry, encoding, mask, focus, look.labels],\n );\n const titles = useTitles(labelledIds);\n const labels = useMemo(\n () => labelledIds.flatMap((vertex) => (titles.get(vertex) ? [{ vertex, text: titles.get(vertex) as string }] : [])),\n [labelledIds, titles],\n );\n useEffect(() => {\n setLabelOrder(labels.map((label) => label.vertex));\n track();\n schedule();\n }, [labels, schedule, setLabelOrder, track, look.grid]);\n\n const gesture = useGesture({\n getGraph,\n getSelection: useCallback(() => api.getState().selection, [api]),\n commit: (vertices, source, label) => api.select(vertices ? [...vertices] : null, source, label),\n setTool: api.setTool,\n tool,\n });\n const { active, drag, preview } = gesture;\n\n const capacity = hostRef.current ? categoricalCapacity(hostRef.current) : undefined;\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n\n return (\n <div\n {...rest}\n className={cn(\"relative isolate size-full overflow-hidden bg-background\", className)}\n data-slot={slot ?? \"graph-canvas\"}\n ref={hostRef}\n >\n <div className=\"size-full\" data-slot=\"graph-canvas-surface\" ref={surfaceRef} />\n <Show when={look.grid}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n ref={gridRef}\n style={{ backgroundImage: \"radial-gradient(var(--border) 1px, transparent 1px)\" }}\n />\n </Show>\n <Show when={look.vignette}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n style={{ background: \"radial-gradient(ellipse 75% 65% at 50% 45%, transparent 30%, var(--background) 100%)\" }}\n />\n </Show>\n <div className=\"pointer-events-none absolute inset-0 overflow-hidden\">\n {labels.map((label) => (\n <span\n className=\"absolute top-0 left-0 whitespace-nowrap font-medium text-[10px] text-foreground leading-none opacity-0 transition-opacity [text-shadow:0_0_3px_var(--background),0_0_6px_var(--background)]\"\n key={String(label.vertex)}\n ref={labelRef(label.vertex)}\n >\n {label.text}\n </span>\n ))}\n <HoverCard\n cardRef={cardRef}\n domain={domain}\n encoding={encoding}\n options={options}\n scale={scale}\n schedule={schedule}\n setHovered={setHovered}\n />\n </div>\n <Show when={active !== null}>\n <div className=\"absolute inset-0 cursor-crosshair\" data-slot=\"graph-canvas-gesture\" {...gesture.handlers}>\n {drag && (\n <svg className=\"pointer-events-none size-full\">\n {drag.tool === \"rect\" ? (\n <rect\n className=\"stroke-primary\"\n height={Math.abs(drag.to[1] - drag.from[1])}\n strokeDasharray=\"4 3\"\n strokeWidth={1}\n style={{ fill: WASH }}\n width={Math.abs(drag.to[0] - drag.from[0])}\n x={Math.min(drag.from[0], drag.to[0])}\n y={Math.min(drag.from[1], drag.to[1])}\n />\n ) : (\n drag.path.length > 1 && (\n <polygon\n className=\"stroke-primary\"\n points={drag.path.map(([x, y]) => `${x},${y}`).join(\" \")}\n strokeWidth={1}\n style={{ fill: WASH }}\n />\n )\n )}\n </svg>\n )}\n <Show when={preview !== null}>\n <span\n className=\"pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums\"\n style={cursorChip(drag)}\n >\n {preview} node{preview === 1 ? \"\" : \"s\"}\n </span>\n </Show>\n </div>\n </Show>\n <Show when={waiting !== undefined}>\n <p\n className=\"pointer-events-none absolute inset-0 grid place-items-center text-muted-foreground text-xs\"\n data-slot=\"graph-canvas-waiting\"\n >\n {waiting}\n </p>\n </Show>\n {children}\n </div>\n );\n}\n\ninterface HoverCardProps {\n cardRef: React.RefObject<HTMLDivElement | null>;\n domain: readonly unknown[];\n encoding: Encoding | null;\n options: GraphOptions;\n scale: ReturnType<typeof scaleOf>;\n schedule: () => void;\n setHovered: (vertex: VertexId | null) => void;\n}\n\n/** The only part that re-renders on a hover: the canvas and its labels do not. */\nfunction HoverCard({ cardRef, domain, encoding, options, scale, schedule, setHovered }: HoverCardProps) {\n const hovered = useGraphState((s) => s.hovered);\n const titles = useTitles(useMemo(() => (hovered === null ? [] : [hovered]), [hovered]));\n useEffect(() => {\n setHovered(hovered);\n schedule();\n }, [hovered, schedule, setHovered, titles]);\n\n if (hovered === null || !encoding || hovered >= encoding.ranks.length) return null;\n const rank = encoding.ranks[hovered] ?? 0;\n const category = rank < domain.length ? nameOf(domain[rank], options.categories) : null;\n return (\n <div\n className=\"absolute top-0 left-0 flex w-max max-w-60 items-center gap-2 rounded-lg border bg-popover px-2.5 py-2 opacity-0 shadow-lg\"\n data-slot=\"graph-canvas-card\"\n ref={cardRef}\n >\n <ShapeGlyph className=\"size-2.5 shrink-0\" color={scale.color(rank)} shape={scale.shape(rank)} />\n <span className=\"truncate font-medium text-popover-foreground text-sm leading-none\">\n {titles.get(hovered) || `#${hovered}`}\n </span>\n <Show when={category !== null}>\n <span className=\"text-muted-foreground text-xs\">{category}</span>\n </Show>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAuBA;AAMA;AACE;AAEA;AACE;AACE;AAEA;AACA;AACA;AACA;AAC+B;AAGnC;AACF;AAGA;AACE;AAMA;AACE;AACA;AACA;AAAgF;AACzB;AACiF;AAEnH;AAGzB;AAEA;AAAiD;AACtC;AAEX;AAYO;AACL;;AAKoC;AAAqB;;AAGtB;AAAqB;AAQxD;AACE;AACA;AACA;AAA8D;AAGhE;AACA;;AACE;AAAY;AAGd;AAAoB;AACqE;AAC1C;AAGhC;AACqG;AAC9F;AAEtB;AACE;AAEA;AAGF;AAA2B;AACzB;AAC+D;AAC+B;AACjF;AACb;AAOF;AACE;AAAC;AAAA;AACK;AAC+E;AAChE;AACd;AAEL;AAA6E;AAE3E;AAAC;AAAA;AACW;AACL;AACqB;AAAsD;AAEpF;AAEE;AAAC;AAAA;AACW;AACW;AAAuF;AAEhH;AAEG;AACC;AAAC;AAAA;AACW;AAEgB;AAEnB;AAAA;AAHiB;AAK3B;AACD;AAAC;AAAA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAEJ;AAGK;AAGK;AAAC;AAAA;AACW;AACgC;AAC1B;AACH;AACE;AAC0B;AACL;AACA;AAAA;AAIpC;AAAC;AAAA;AACW;AAC6C;AAC1C;AACE;AAAK;AAI5B;AAGA;AAAC;AAAA;AACW;AACY;AAErB;AAAA;AAAQ;AAA2B;AAAA;AAAA;AAExC;AAEJ;AAEE;AAAC;AAAA;AACW;AACA;AAET;AAAA;AAEL;AACC;AAAA;AAAA;AAGP;AAaA;AACE;AAOA;AAJE;AACA;AAIF;AAEA;AACE;AAAC;AAAA;AACW;AACA;AACL;AAEL;AAA8F;AAG9F;AAGA;AAAA;AAAA;AAGN;;;;"}
1
+ {"version":3,"file":"graph-canvas.js","sources":["../../src/parts/graph-canvas.tsx"],"sourcesContent":["\"use client\";\n\nimport { categoricalCapacity, cn, Show, useThemeTick } from \"@kanzo-tech/ui\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { readTitles } from \"../core/detail\";\nimport type { Encoding, Geometry } from \"../core/load\";\nimport type { GraphOptions } from \"../core/state\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { internalsOf } from \"../react/use-graph\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\nimport { resolveLook } from \"../render/graph-looks\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { cursorChip, useGesture } from \"./gesture\";\nimport { useOverlays } from \"./overlays\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphCanvasProps extends React.ComponentProps<\"div\"> {\n /** The chrome — a toolbar, a legend, an inspector — positioned over the surface. */\n children?: ReactNode;\n}\n\nconst WASH = \"color-mix(in oklab, var(--primary) 17%, transparent)\";\n\n/**\n * The biggest `budget` surviving vertices by the ramp, the focused one first. One pass, keeping the\n * best few in order: a sort of every vertex to name twenty-six of them is the cost this avoids.\n */\nfunction labelled(geometry: Geometry, encoding: Encoding, mask: Uint8Array | null, budget: number, focus: VertexId | null): VertexId[] {\n const ramp = encoding.sizes;\n const best: VertexId[] = [];\n if (ramp && budget > 0) {\n for (let id = 0; id < geometry.size; id++) {\n const value = ramp[id] as number;\n if ((mask && !mask[id]) || Number.isNaN(value)) continue;\n if (best.length === budget && value <= (ramp[best[budget - 1] as number] as number)) continue;\n let at = best.length;\n while (at > 0 && (ramp[best[at - 1] as number] as number) < value) at--;\n best.splice(at, 0, id);\n if (best.length > budget) best.pop();\n }\n }\n return focus === null || best.includes(focus) ? best : [focus, ...best];\n}\n\n/** A label's text is read for the vertices that carry one, never carried for every vertex. */\nfunction useTitles(vertices: readonly VertexId[]): ReadonlyMap<VertexId, string> {\n const api = useGraphContext();\n const corpus = useGraphState((s) => s.corpus);\n const title = useGraphState((s) => s.options.title);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const [titles, setTitles] = useState<ReadonlyMap<VertexId, string>>(() => new Map());\n const key = vertices.join(\",\");\n useEffect(() => {\n if (!corpus || !geometry || key === \"\") return;\n const aborter = new AbortController();\n readTitles(corpus, geometry, key.split(\",\").map(Number), title, aborter.signal).then(\n (found) => !aborter.signal.aborted && setTitles(found),\n (error: unknown) => !aborter.signal.aborted && api.getState().options.onFailure(error),\n );\n return () => aborter.abort();\n }, [api, corpus, geometry, key, title]);\n return titles;\n}\n\nconst WAITING: Partial<Record<string, string>> = {\n opening: \"Opening the corpus…\",\n loading: \"Loading the graph…\",\n};\n\n/**\n * **The element, and what the look declares over it** — the grid, the vignette, the labels and the\n * hover card are fields of `Look`, so drawing them is the canvas's; the marquee and lasso gesture is\n * too. Two layers: cosmos.gl fills the surface with a canvas of its own, so chrome parented there\n * would be a sibling inside an element the renderer resizes. The frame is `isolate`, so a host's\n * `z-index` on a child cannot escape into the page.\n *\n * The surface is never gated on the first answer: rendered behind a placeholder it deadlocks — no\n * element, no renderer, no camera, no question. Waiting is an overlay over an empty canvas.\n */\nexport function GraphCanvas({ children, className, slot, ...rest }: GraphCanvasProps) {\n const api = useGraphContext();\n const { attach, renderer } = internalsOf(api);\n const focus = useGraphState((s) => s.focus);\n const tool = useGraphState((s) => s.tool);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const waiting = useGraphState((s) => (s.drawn === null ? WAITING[s.status] : undefined));\n const look = useMemo(() => resolveLook(options.look), [options.look]);\n const getGraph = useCallback(() => renderer()?.graph ?? null, [renderer]);\n const overlays = useOverlays({ getGraph });\n const { cardRef, gridRef, hostRef, hoverAt, labelRef, schedule, setHovered, setLabelOrder, track } = overlays;\n const surfaceRef = useRef<HTMLDivElement>(null);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const encoding = useGraphSnapshot((s) => s.encoding);\n const mask = useGraphSnapshot((s) => s.mask);\n\n useEffect(() => {\n const surface = surfaceRef.current;\n if (!surface) return;\n return attach(surface, { onFrame: schedule, onHover: hoverAt });\n }, [attach, hoverAt, schedule]);\n\n const themeTick = useThemeTick();\n useEffect(() => {\n renderer()?.repaint();\n }, [renderer, themeTick]);\n\n const labelledIds = useMemo(\n () => (geometry && encoding ? labelled(geometry, encoding, mask, look.labels, focus) : []),\n [geometry, encoding, mask, focus, look.labels],\n );\n const titles = useTitles(labelledIds);\n const labels = useMemo(\n () => labelledIds.flatMap((vertex) => (titles.get(vertex) ? [{ vertex, text: titles.get(vertex) as string }] : [])),\n [labelledIds, titles],\n );\n useEffect(() => {\n setLabelOrder(labels.map((label) => label.vertex));\n track();\n schedule();\n }, [labels, schedule, setLabelOrder, track, look.grid]);\n\n const gesture = useGesture({\n getGraph,\n getSelection: useCallback(() => api.getState().selection, [api]),\n commit: (vertices, source, label) => api.select(vertices ? [...vertices] : null, source, label),\n setTool: api.setTool,\n tool,\n });\n const { active, drag, preview } = gesture;\n\n const capacity = hostRef.current ? categoricalCapacity(hostRef.current) : undefined;\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n\n return (\n <div\n {...rest}\n className={cn(\"relative isolate size-full overflow-hidden bg-background\", className)}\n data-slot={slot ?? \"graph-canvas\"}\n ref={hostRef}\n >\n <div className=\"size-full\" data-slot=\"graph-canvas-surface\" ref={surfaceRef} />\n <Show when={look.grid}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n ref={gridRef}\n style={{ backgroundImage: \"radial-gradient(var(--border) 1px, transparent 1px)\" }}\n />\n </Show>\n <Show when={look.vignette}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n style={{ background: \"radial-gradient(ellipse 75% 65% at 50% 45%, transparent 30%, var(--background) 100%)\" }}\n />\n </Show>\n <div className=\"pointer-events-none absolute inset-0 overflow-hidden\">\n {labels.map((label) => (\n <span\n className=\"absolute top-0 left-0 whitespace-nowrap font-medium text-[10px] text-foreground leading-none opacity-0 transition-opacity [text-shadow:0_0_3px_var(--background),0_0_6px_var(--background)]\"\n key={String(label.vertex)}\n ref={labelRef(label.vertex)}\n >\n {label.text}\n </span>\n ))}\n <HoverCard\n cardRef={cardRef}\n domain={domain}\n encoding={encoding}\n options={options}\n scale={scale}\n schedule={schedule}\n setHovered={setHovered}\n />\n </div>\n <Show when={active !== null}>\n <div className=\"absolute inset-0 cursor-crosshair\" data-slot=\"graph-canvas-gesture\" {...gesture.handlers}>\n {drag && (\n <svg className=\"pointer-events-none size-full\">\n {drag.tool === \"rect\" ? (\n <rect\n className=\"stroke-primary\"\n height={Math.abs(drag.to[1] - drag.from[1])}\n strokeDasharray=\"4 3\"\n strokeWidth={1}\n style={{ fill: WASH }}\n width={Math.abs(drag.to[0] - drag.from[0])}\n x={Math.min(drag.from[0], drag.to[0])}\n y={Math.min(drag.from[1], drag.to[1])}\n />\n ) : (\n drag.path.length > 1 && (\n <polygon\n className=\"stroke-primary\"\n points={drag.path.map(([x, y]) => `${x},${y}`).join(\" \")}\n strokeWidth={1}\n style={{ fill: WASH }}\n />\n )\n )}\n </svg>\n )}\n <Show when={preview !== null}>\n <span\n className=\"pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums\"\n style={cursorChip(drag)}\n >\n {preview} node{preview === 1 ? \"\" : \"s\"}\n </span>\n </Show>\n </div>\n </Show>\n <Show when={waiting !== undefined}>\n <p\n className=\"pointer-events-none absolute inset-0 grid place-items-center text-muted-foreground text-xs\"\n data-slot=\"graph-canvas-waiting\"\n >\n {waiting}\n </p>\n </Show>\n {children}\n </div>\n );\n}\n\ninterface HoverCardProps {\n cardRef: React.RefObject<HTMLDivElement | null>;\n domain: readonly unknown[];\n encoding: Encoding | null;\n options: GraphOptions;\n scale: ReturnType<typeof scaleOf>;\n schedule: () => void;\n setHovered: (vertex: VertexId | null) => void;\n}\n\n/** The only part that re-renders on a hover: the canvas and its labels do not. */\nfunction HoverCard({ cardRef, domain, encoding, options, scale, schedule, setHovered }: HoverCardProps) {\n const hovered = useGraphState((s) => s.hovered);\n const titles = useTitles(useMemo(() => (hovered === null ? [] : [hovered]), [hovered]));\n useEffect(() => {\n setHovered(hovered);\n schedule();\n }, [hovered, schedule, setHovered, titles]);\n\n if (hovered === null || !encoding || hovered >= encoding.ranks.length) return null;\n const rank = encoding.ranks[hovered] ?? 0;\n const category = rank < domain.length ? nameOf(domain[rank], options.categories) : null;\n return (\n <div\n className=\"absolute top-0 left-0 flex w-max max-w-60 items-center gap-2 rounded-lg border bg-popover px-2.5 py-2 opacity-0 shadow-lg\"\n data-slot=\"graph-canvas-card\"\n ref={cardRef}\n >\n <ShapeGlyph className=\"size-2.5 shrink-0\" color={scale.color(rank)} shape={scale.shape(rank)} />\n <span className=\"truncate font-medium text-popover-foreground text-sm leading-none\">\n {titles.get(hovered) || `#${hovered}`}\n </span>\n <Show when={category !== null}>\n <span className=\"text-muted-foreground text-xs\">{category}</span>\n </Show>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAuBA;AAMA;AACE;AAEA;AACE;AACE;AAEA;AACA;AACA;AACA;AAC+B;AAGnC;AACF;AAGA;AACE;AAMA;AACE;AACA;AACA;AAAgF;AACzB;AACgC;AAElE;AAGzB;AAEA;AAAiD;AACtC;AAEX;AAYO;AACL;;AAKoC;AAAqB;;AAGtB;AAAqB;AAQxD;AACE;AACA;AACA;AAA8D;AAGhE;AACA;;AACE;AAAY;AAGd;AAAoB;AACqE;AAC1C;AAGhC;AACqG;AAC9F;AAEtB;AACE;AAEA;AAGF;AAA2B;AACzB;AAC+D;AAC+B;AACjF;AACb;AAOF;AACE;AAAC;AAAA;AACK;AAC+E;AAChE;AACd;AAEL;AAA6E;AAE3E;AAAC;AAAA;AACW;AACL;AACqB;AAAsD;AAEpF;AAEE;AAAC;AAAA;AACW;AACW;AAAuF;AAEhH;AAEG;AACC;AAAC;AAAA;AACW;AAEgB;AAEnB;AAAA;AAHiB;AAK3B;AACD;AAAC;AAAA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAEJ;AAGK;AAGK;AAAC;AAAA;AACW;AACgC;AAC1B;AACH;AACE;AAC0B;AACL;AACA;AAAA;AAIpC;AAAC;AAAA;AACW;AAC6C;AAC1C;AACE;AAAK;AAI5B;AAGA;AAAC;AAAA;AACW;AACY;AAErB;AAAA;AAAQ;AAA2B;AAAA;AAAA;AAExC;AAEJ;AAEE;AAAC;AAAA;AACW;AACA;AAET;AAAA;AAEL;AACC;AAAA;AAAA;AAGP;AAaA;AACE;AAOA;AAJE;AACA;AAIF;AAEA;AACE;AAAC;AAAA;AACW;AACA;AACL;AAEL;AAA8F;AAG9F;AAGA;AAAA;AAAA;AAGN;;;;"}
@@ -0,0 +1,15 @@
1
+ export interface GraphCountsProps extends React.ComponentProps<"p"> {
2
+ /** Draws a spinner while the corpus opens, the graph loads or a layout runs — for a host with no status of its own beside it. */
3
+ spinner?: boolean;
4
+ }
5
+ /**
6
+ * **What is drawn of the whole, in one sentence** — "1.2K of 5K nodes drawn · 8K edges". `drawn`
7
+ * shrinks with the page's filter and `total` is the manifest's, so the sentence is the filter's
8
+ * effect; an edge counts when both its ends are drawn. A figure not yet known is "—", and the
9
+ * numbers are compact in the nearest `LocaleProvider`'s locale.
10
+ *
11
+ * `aria-busy` follows `status`, because only a load changes these figures; a running layout moves
12
+ * the points and not the counts, so it reaches the spinner alone, named "Laying out".
13
+ */
14
+ export declare function GraphCounts({ className, slot, spinner, ...rest }: GraphCountsProps): import("react").JSX.Element;
15
+ //# sourceMappingURL=graph-counts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-counts.d.ts","sourceRoot":"","sources":["../../src/parts/graph-counts.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC;IACjE,iIAAiI;IACjI,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAQD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,OAAe,EAAE,GAAG,IAAI,EAAE,EAAE,gBAAgB,+BAsB1F"}
@@ -0,0 +1,41 @@
1
+ "use client";
2
+ import { jsxs as i, jsx as n } from "react/jsx-runtime";
3
+ import { Show as f, Spinner as h, cn as x, FormatNumber as b } from "@kanzo-tech/ui";
4
+ import { useGraphState as e } from "../react/use-graph-state.js";
5
+ const v = { notation: "compact", maximumFractionDigits: 1 };
6
+ function r({ value: o }) {
7
+ return o == null ? "—" : /* @__PURE__ */ n(b, { value: o, ...v });
8
+ }
9
+ function N({ className: o, slot: u, spinner: c = !1, ...l }) {
10
+ const m = e((t) => {
11
+ var a;
12
+ return (a = t.drawn) == null ? void 0 : a.vertices;
13
+ }), d = e((t) => {
14
+ var a;
15
+ return (a = t.drawn) == null ? void 0 : a.edges;
16
+ }), g = e((t) => t.total), s = e((t) => t.status === "opening" || t.status === "loading"), p = e((t) => t.motion === "running");
17
+ return /* @__PURE__ */ i(
18
+ "p",
19
+ {
20
+ "aria-busy": s || void 0,
21
+ ...l,
22
+ className: x("flex items-center gap-1.5 text-muted-foreground text-xs tabular-nums", o),
23
+ "data-slot": u ?? "graph-counts",
24
+ children: [
25
+ /* @__PURE__ */ n(f, { when: c && (s || p), children: /* @__PURE__ */ n(h, { "aria-label": s ? "Loading" : "Laying out", className: "size-3" }) }),
26
+ /* @__PURE__ */ i("span", { children: [
27
+ /* @__PURE__ */ n(r, { value: m }),
28
+ " of ",
29
+ /* @__PURE__ */ n(r, { value: g }),
30
+ " nodes drawn · ",
31
+ /* @__PURE__ */ n(r, { value: d }),
32
+ " edges"
33
+ ] })
34
+ ]
35
+ }
36
+ );
37
+ }
38
+ export {
39
+ N as GraphCounts
40
+ };
41
+ //# sourceMappingURL=graph-counts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-counts.js","sources":["../../src/parts/graph-counts.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn, FormatNumber, Show, Spinner } from \"@kanzo-tech/ui\";\nimport { useGraphState } from \"../react/use-graph-state\";\n\nexport interface GraphCountsProps extends React.ComponentProps<\"p\"> {\n /** Draws a spinner while the corpus opens, the graph loads or a layout runs — for a host with no status of its own beside it. */\n spinner?: boolean;\n}\n\nconst COMPACT = { notation: \"compact\", maximumFractionDigits: 1 } as const;\n\nfunction Count({ value }: { value: number | null | undefined }) {\n return value === null || value === undefined ? \"—\" : <FormatNumber value={value} {...COMPACT} />;\n}\n\n/**\n * **What is drawn of the whole, in one sentence** — \"1.2K of 5K nodes drawn · 8K edges\". `drawn`\n * shrinks with the page's filter and `total` is the manifest's, so the sentence is the filter's\n * effect; an edge counts when both its ends are drawn. A figure not yet known is \"—\", and the\n * numbers are compact in the nearest `LocaleProvider`'s locale.\n *\n * `aria-busy` follows `status`, because only a load changes these figures; a running layout moves\n * the points and not the counts, so it reaches the spinner alone, named \"Laying out\".\n */\nexport function GraphCounts({ className, slot, spinner = false, ...rest }: GraphCountsProps) {\n const vertices = useGraphState((s) => s.drawn?.vertices);\n const edges = useGraphState((s) => s.drawn?.edges);\n const total = useGraphState((s) => s.total);\n const busy = useGraphState((s) => s.status === \"opening\" || s.status === \"loading\");\n const laying = useGraphState((s) => s.motion === \"running\");\n\n return (\n <p\n aria-busy={busy || undefined}\n {...rest}\n className={cn(\"flex items-center gap-1.5 text-muted-foreground text-xs tabular-nums\", className)}\n data-slot={slot ?? \"graph-counts\"}\n >\n <Show when={spinner && (busy || laying)}>\n <Spinner aria-label={busy ? \"Loading\" : \"Laying out\"} className=\"size-3\" />\n </Show>\n <span>\n <Count value={vertices} /> of <Count value={total} /> nodes drawn · <Count value={edges} /> edges\n </span>\n </p>\n );\n}\n"],"names":[],"mappings":";;;;AAUA;AAEA;AACE;AACF;AAWO;AACL;;AAAsC;AAAS;;AACZ;AAAS;AAK5C;AACE;AAAC;AAAA;AACoB;AACf;AAC2F;AAC5E;AAEnB;AAEA;AAEE;AAAwB;AAAE;AAAyB;AAAE;AAAoC;AAAE;AAC7F;AAAA;AAAA;AAGN;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph-inspector.d.ts","sourceRoot":"","sources":["../../src/parts/graph-inspector.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGrE,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAO/D,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC;IACxF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,SAAS,CAAC;CAChD;AAWD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,mBAAmB,+BAuFzF"}
1
+ {"version":3,"file":"graph-inspector.d.ts","sourceRoot":"","sources":["../../src/parts/graph-inspector.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGrE,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAO/D,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC;IACxF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,SAAS,CAAC;CAChD;AAYD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,mBAAmB,+BA0FzF"}
@@ -1,82 +1,82 @@
1
1
  "use client";
2
- import { jsxs as i, jsx as n, Fragment as O } from "react/jsx-runtime";
3
- import { useChartCapacity as B, Show as m, Skeleton as F, Badge as M, Button as T, DataList as j, DataListItem as A, DataListItemLabel as E, DataListItemValue as V, cn as $ } from "@kanzo-tech/ui";
4
- import { CrosshairIcon as q } from "lucide-react";
5
- import { useMemo as H, useState as J, useEffect as K } from "react";
6
- import { nameOf as P } from "../core/categories.js";
7
- import { bindingOf as Q } from "../core/channels.js";
8
- import { readVertex as R } from "../core/detail.js";
9
- import { useGraphContext as U } from "../react/graph-root.js";
10
- import { useGraphState as d, useGraphSnapshot as W } from "../react/use-graph-state.js";
11
- import { scaleOf as X } from "../render/graph-model.js";
12
- import { ShapeGlyph as Y } from "./shape-glyph.js";
13
- const w = (s) => s instanceof Date ? s.toISOString().slice(0, 10) : s == null || s === "" ? "—" : String(s);
14
- function ct({ children: s, className: C, slot: I, ...k }) {
15
- var y;
16
- const p = U(), o = d((t) => t.focus), h = d((t) => t.corpus), r = W((t) => t.geometry), l = d((t) => t.options), D = d((t) => {
17
- var a;
18
- return ((a = t.drawn) == null ? void 0 : a.domain) ?? t.domain;
19
- }), g = B(), v = H(() => X(l, g), [l, g]), [f, b] = J(null);
20
- K(() => {
21
- if (o === null || !h || !r) return;
2
+ import { jsxs as i, jsx as n, Fragment as T } from "react/jsx-runtime";
3
+ import { useChartCapacity as B, Show as d, Skeleton as F, Badge as M, Button as j, DataList as A, DataListItem as V, DataListItemLabel as E, DataListItemValue as $, cn as q } from "@kanzo-tech/ui";
4
+ import { CrosshairIcon as H } from "lucide-react";
5
+ import { useMemo as J, useState as K, useEffect as P } from "react";
6
+ import { nameOf as Q } from "../core/categories.js";
7
+ import { bindingOf as R } from "../core/channels.js";
8
+ import { readVertex as U } from "../core/detail.js";
9
+ import { useGraphContext as W } from "../react/graph-root.js";
10
+ import { useGraphState as p, useGraphSnapshot as X } from "../react/use-graph-state.js";
11
+ import { scaleOf as Y } from "../render/graph-model.js";
12
+ import { ShapeGlyph as Z } from "./shape-glyph.js";
13
+ const C = (a) => a instanceof Date ? a.toISOString().slice(0, 10) : a == null || a === "" ? "—" : String(a);
14
+ function mt({ children: a, className: I, slot: k, ...D }) {
15
+ var S;
16
+ const h = W(), s = p((t) => t.focus), u = p((t) => t.corpus), r = X((t) => t.geometry), l = p((t) => t.options), G = p((t) => {
17
+ var o;
18
+ return ((o = t.drawn) == null ? void 0 : o.domain) ?? t.domain;
19
+ }), v = B(), b = J(() => Y(l, v), [l, v]), [f, N] = K(null);
20
+ P(() => {
21
+ if (s === null || !u || !r) return;
22
22
  const t = new AbortController();
23
- return R(h, r, o, t.signal).then(
24
- (a) => !t.signal.aborted && b({ vertex: o, detail: a }),
25
- (a) => {
26
- t.signal.aborted || (b({ vertex: o, detail: null }), p.getState().options.onFailure(a instanceof Error ? a.message : String(a)));
23
+ return U(u, r, s, t.signal).then(
24
+ (o) => !t.signal.aborted && N({ vertex: s, detail: o }),
25
+ (o) => {
26
+ t.signal.aborted || (N({ vertex: s, detail: !1 }), h.getState().options.onFailure(o));
27
27
  }
28
28
  ), () => t.abort();
29
- }, [p, h, o, r]);
30
- const e = f !== null && f.vertex === o ? f.detail : void 0, u = (t) => {
31
- var a;
32
- return (a = e == null ? void 0 : e.fields.find((z) => z.name === t)) == null ? void 0 : a.value;
33
- }, c = Q(l), N = c.byTable ? e == null ? void 0 : e.table : u(c.category), S = D.findIndex((t) => String(t) === String(N)), G = c.byTable || c.category !== void 0, L = (y = r == null ? void 0 : r.tables.find((t) => t.name === (e == null ? void 0 : e.table))) == null ? void 0 : y.identity, x = u(l.title) ?? u(L);
34
- return /* @__PURE__ */ i("div", { ...k, className: $("space-y-3 text-sm", C), "data-slot": I ?? "graph-inspector", children: [
35
- /* @__PURE__ */ n(m, { when: o === null, children: /* @__PURE__ */ n("p", { className: "text-muted-foreground text-xs", children: "Click a vertex on the canvas to inspect it." }) }),
36
- /* @__PURE__ */ n(m, { when: o !== null && e === void 0, children: /* @__PURE__ */ n(F, { className: "h-24 w-full" }) }),
37
- /* @__PURE__ */ n(m, { when: o !== null && e === null, children: /* @__PURE__ */ n("p", { className: "text-muted-foreground text-xs", children: "This vertex could not be read." }) }),
38
- e && /* @__PURE__ */ i(O, { children: [
29
+ }, [h, u, s, r]);
30
+ const c = f !== null && f.vertex === s ? f.detail : void 0, e = c || null, x = (t) => {
31
+ var o;
32
+ return (o = e == null ? void 0 : e.fields.find((O) => O.name === t)) == null ? void 0 : o.value;
33
+ }, m = R(l), w = m.byTable ? e == null ? void 0 : e.table : x(m.category), y = G.findIndex((t) => String(t) === String(w)), L = m.byTable || m.category !== void 0, z = (S = r == null ? void 0 : r.tables.find((t) => t.name === (e == null ? void 0 : e.table))) == null ? void 0 : S.identity, g = x(l.title) ?? x(z);
34
+ return /* @__PURE__ */ i("div", { ...D, className: q("space-y-3 text-sm", I), "data-slot": k ?? "graph-inspector", children: [
35
+ /* @__PURE__ */ n(d, { when: s === null, children: /* @__PURE__ */ n("p", { className: "text-muted-foreground text-xs", children: "Click a vertex on the canvas to inspect it." }) }),
36
+ /* @__PURE__ */ n(d, { when: s !== null && c === void 0, children: /* @__PURE__ */ n(F, { className: "h-24 w-full" }) }),
37
+ /* @__PURE__ */ n(d, { when: s !== null && c !== void 0 && !e, children: /* @__PURE__ */ n("p", { className: "text-muted-foreground text-xs", children: c === null ? "This vertex is not in the corpus." : "This vertex could not be read." }) }),
38
+ e && /* @__PURE__ */ i(T, { children: [
39
39
  /* @__PURE__ */ i("div", { children: [
40
- /* @__PURE__ */ n("p", { className: "truncate font-medium", "data-slot": "graph-inspector-title", children: x == null ? `#${e.vertex}` : w(x) }),
40
+ /* @__PURE__ */ n("p", { className: "truncate font-medium", "data-slot": "graph-inspector-title", children: g == null ? `#${e.vertex}` : C(g) }),
41
41
  /* @__PURE__ */ i("div", { className: "mt-1 flex items-center gap-1.5", children: [
42
- /* @__PURE__ */ n(m, { when: G, children: /* @__PURE__ */ i(M, { className: "gap-1 text-[10px]", size: "xs", variant: "outline", children: [
42
+ /* @__PURE__ */ n(d, { when: L, children: /* @__PURE__ */ i(M, { className: "gap-1 text-[10px]", size: "xs", variant: "outline", children: [
43
43
  /* @__PURE__ */ n(
44
- Y,
44
+ Z,
45
45
  {
46
46
  className: "size-2 shrink-0",
47
- color: v.color(Math.max(0, S)),
48
- shape: v.shape(Math.max(0, S))
47
+ color: b.color(Math.max(0, y)),
48
+ shape: b.shape(Math.max(0, y))
49
49
  }
50
50
  ),
51
- P(N, l.categories)
51
+ Q(w, l.categories)
52
52
  ] }) }),
53
53
  /* @__PURE__ */ i(
54
- T,
54
+ j,
55
55
  {
56
56
  className: "h-5 gap-1 text-[10px]",
57
- onClick: () => p.reveal(e.vertex),
57
+ onClick: () => h.reveal(e.vertex),
58
58
  size: "sm",
59
59
  title: "Bring this vertex into view on the canvas",
60
60
  variant: "ghost",
61
61
  children: [
62
- /* @__PURE__ */ n(q, { className: "size-3" }),
62
+ /* @__PURE__ */ n(H, { className: "size-3" }),
63
63
  "Find on canvas"
64
64
  ]
65
65
  }
66
66
  )
67
67
  ] })
68
68
  ] }),
69
- /* @__PURE__ */ i(j, { orientation: "vertical", children: [
70
- e.fields.map((t) => /* @__PURE__ */ i(A, { className: "gap-0.5 py-0", children: [
69
+ /* @__PURE__ */ i(A, { orientation: "vertical", children: [
70
+ e.fields.map((t) => /* @__PURE__ */ i(V, { className: "gap-0.5 py-0", children: [
71
71
  /* @__PURE__ */ n(E, { className: "text-xs", children: t.name }),
72
- /* @__PURE__ */ n(V, { className: "break-all", children: w(t.value) })
72
+ /* @__PURE__ */ n($, { className: "break-all", children: C(t.value) })
73
73
  ] }, t.name)),
74
- s == null ? void 0 : s(e)
74
+ a == null ? void 0 : a(e)
75
75
  ] })
76
76
  ] })
77
77
  ] });
78
78
  }
79
79
  export {
80
- ct as GraphInspector
80
+ mt as GraphInspector
81
81
  };
82
82
  //# sourceMappingURL=graph-inspector.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-inspector.js","sources":["../../src/parts/graph-inspector.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Badge,\n Button,\n cn,\n DataList,\n DataListItem,\n DataListItemLabel,\n DataListItemValue,\n Show,\n Skeleton,\n useChartCapacity,\n} from \"@kanzo-tech/ui\";\nimport { CrosshairIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { bindingOf } from \"../core/channels\";\nimport { readVertex, type VertexDetail } from \"../core/detail\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphInspectorProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /**\n * More fields for the focused vertex, drawn inside its list after the corpus's own — a\n * `DataListItem` each. A render prop rather than a table of formatters: what a product adds is\n * markup, and the part never learns what any of it means.\n */\n children?: (detail: VertexDetail) => ReactNode;\n}\n\n/** A cell, as text — a `DATE` arrives as a `Date`, and a 64-bit column as a `bigint`. */\nconst text = (value: unknown): string => {\n if (value instanceof Date) return value.toISOString().slice(0, 10);\n if (value === null || value === undefined || value === \"\") return \"—\";\n return String(value);\n};\n\ntype Answer = { vertex: VertexId; detail: VertexDetail | null } | null;\n\n/**\n * **The focused vertex, fetched and laid out by the corpus's own tables.** The loaded graph carries\n * what the channels project and nothing else, so the rest of the row is read when a reader focuses\n * it — a scan of its table, filtered to its key, cancelled when the focus moves on. The fields are the\n * table's, in the order the manifest declares them; the key and the position are the canvas's.\n */\nexport function GraphInspector({ children, className, slot, ...rest }: GraphInspectorProps) {\n const api = useGraphContext();\n const focus = useGraphState((s) => s.focus);\n const corpus = useGraphState((s) => s.corpus);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const capacity = useChartCapacity();\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n const [answer, setAnswer] = useState<Answer>(null);\n\n useEffect(() => {\n if (focus === null || !corpus || !geometry) return;\n const aborter = new AbortController();\n readVertex(corpus, geometry, focus, aborter.signal).then(\n (detail) => !aborter.signal.aborted && setAnswer({ vertex: focus, detail }),\n (error: unknown) => {\n if (aborter.signal.aborted) return;\n setAnswer({ vertex: focus, detail: null });\n api.getState().options.onFailure(error instanceof Error ? error.message : String(error));\n },\n );\n return () => aborter.abort();\n }, [api, corpus, focus, geometry]);\n\n const current = answer !== null && answer.vertex === focus ? answer.detail : undefined;\n const field = (name: string | undefined) => current?.fields.find((f) => f.name === name)?.value;\n const binding = bindingOf(options);\n const category = binding.byTable ? current?.table : field(binding.category);\n const rank = domain.findIndex((value) => String(value) === String(category));\n const bound = binding.byTable || binding.category !== undefined;\n const identity = geometry?.tables.find((table) => table.name === current?.table)?.identity;\n const heading = field(options.title) ?? field(identity);\n\n return (\n <div {...rest} className={cn(\"space-y-3 text-sm\", className)} data-slot={slot ?? \"graph-inspector\"}>\n <Show when={focus === null}>\n <p className=\"text-muted-foreground text-xs\">Click a vertex on the canvas to inspect it.</p>\n </Show>\n <Show when={focus !== null && current === undefined}>\n <Skeleton className=\"h-24 w-full\" />\n </Show>\n <Show when={focus !== null && current === null}>\n <p className=\"text-muted-foreground text-xs\">This vertex could not be read.</p>\n </Show>\n {current && (\n <>\n <div>\n <p className=\"truncate font-medium\" data-slot=\"graph-inspector-title\">\n {heading === undefined || heading === null ? `#${current.vertex}` : text(heading)}\n </p>\n <div className=\"mt-1 flex items-center gap-1.5\">\n <Show when={bound}>\n <Badge className=\"gap-1 text-[10px]\" size=\"xs\" variant=\"outline\">\n <ShapeGlyph\n className=\"size-2 shrink-0\"\n color={scale.color(Math.max(0, rank))}\n shape={scale.shape(Math.max(0, rank))}\n />\n {nameOf(category, options.categories)}\n </Badge>\n </Show>\n <Button\n className=\"h-5 gap-1 text-[10px]\"\n onClick={() => api.reveal(current.vertex)}\n size=\"sm\"\n title=\"Bring this vertex into view on the canvas\"\n variant=\"ghost\"\n >\n <CrosshairIcon className=\"size-3\" />\n Find on canvas\n </Button>\n </div>\n </div>\n <DataList orientation=\"vertical\">\n {current.fields.map((f) => (\n <DataListItem className=\"gap-0.5 py-0\" key={f.name}>\n <DataListItemLabel className=\"text-xs\">{f.name}</DataListItemLabel>\n <DataListItemValue className=\"break-all\">{text(f.value)}</DataListItemValue>\n </DataListItem>\n ))}\n {children?.(current)}\n </DataList>\n </>\n )}\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAmCA;AAcO;;AACL;;AAKoC;AAAqB;AAKzD;AACE;AACA;AACA;AAAoD;AACwB;AAExE;AAEuF;AACzF;AAEmB;AAGvB;;AAC4C;AAA8C;AAQ1F;AAEI;AAEA;AAGA;AAGA;AAGI;AACE;AAEA;AAEE;AAEI;AAAA;AAAC;AAAA;AACW;AAC0B;AACA;AAAA;AAAA;AAEF;AAExC;AACA;AAAC;AAAA;AACW;AAC8B;AACnC;AACC;AACE;AAER;AAAkC;AAAE;AAAA;AAAA;AAAA;AAGxC;AACF;AAEG;AAEG;AAA+C;AACS;AAE3D;AACW;AACd;AACF;AAIR;;;;"}
1
+ {"version":3,"file":"graph-inspector.js","sources":["../../src/parts/graph-inspector.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Badge,\n Button,\n cn,\n DataList,\n DataListItem,\n DataListItemLabel,\n DataListItemValue,\n Show,\n Skeleton,\n useChartCapacity,\n} from \"@kanzo-tech/ui\";\nimport { CrosshairIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { bindingOf } from \"../core/channels\";\nimport { readVertex, type VertexDetail } from \"../core/detail\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphInspectorProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /**\n * More fields for the focused vertex, drawn inside its list after the corpus's own — a\n * `DataListItem` each. A render prop rather than a table of formatters: what a product adds is\n * markup, and the part never learns what any of it means.\n */\n children?: (detail: VertexDetail) => ReactNode;\n}\n\n/** A cell, as text — a `DATE` arrives as a `Date`, and a 64-bit column as a `bigint`. */\nconst text = (value: unknown): string => {\n if (value instanceof Date) return value.toISOString().slice(0, 10);\n if (value === null || value === undefined || value === \"\") return \"—\";\n return String(value);\n};\n\n/** `null` is no such vertex in the corpus; `false` is a read that did not answer. */\ntype Answer = { vertex: VertexId; detail: VertexDetail | null | false } | null;\n\n/**\n * **The focused vertex, fetched and laid out by the corpus's own tables.** The loaded graph carries\n * what the channels project and nothing else, so the rest of the row is read when a reader focuses\n * it — a scan of its table, filtered to its key, cancelled when the focus moves on. The fields are the\n * table's, in the order the manifest declares them; the key and the position are the canvas's.\n */\nexport function GraphInspector({ children, className, slot, ...rest }: GraphInspectorProps) {\n const api = useGraphContext();\n const focus = useGraphState((s) => s.focus);\n const corpus = useGraphState((s) => s.corpus);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const capacity = useChartCapacity();\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n const [answer, setAnswer] = useState<Answer>(null);\n\n useEffect(() => {\n if (focus === null || !corpus || !geometry) return;\n const aborter = new AbortController();\n readVertex(corpus, geometry, focus, aborter.signal).then(\n (detail) => !aborter.signal.aborted && setAnswer({ vertex: focus, detail }),\n (error: unknown) => {\n if (aborter.signal.aborted) return;\n setAnswer({ vertex: focus, detail: false });\n api.getState().options.onFailure(error);\n },\n );\n return () => aborter.abort();\n }, [api, corpus, focus, geometry]);\n\n const answered = answer !== null && answer.vertex === focus ? answer.detail : undefined;\n const current = answered || null;\n const field = (name: string | undefined) => current?.fields.find((f) => f.name === name)?.value;\n const binding = bindingOf(options);\n const category = binding.byTable ? current?.table : field(binding.category);\n const rank = domain.findIndex((value) => String(value) === String(category));\n const bound = binding.byTable || binding.category !== undefined;\n const identity = geometry?.tables.find((table) => table.name === current?.table)?.identity;\n const heading = field(options.title) ?? field(identity);\n\n return (\n <div {...rest} className={cn(\"space-y-3 text-sm\", className)} data-slot={slot ?? \"graph-inspector\"}>\n <Show when={focus === null}>\n <p className=\"text-muted-foreground text-xs\">Click a vertex on the canvas to inspect it.</p>\n </Show>\n <Show when={focus !== null && answered === undefined}>\n <Skeleton className=\"h-24 w-full\" />\n </Show>\n <Show when={focus !== null && answered !== undefined && !current}>\n <p className=\"text-muted-foreground text-xs\">\n {answered === null ? \"This vertex is not in the corpus.\" : \"This vertex could not be read.\"}\n </p>\n </Show>\n {current && (\n <>\n <div>\n <p className=\"truncate font-medium\" data-slot=\"graph-inspector-title\">\n {heading === undefined || heading === null ? `#${current.vertex}` : text(heading)}\n </p>\n <div className=\"mt-1 flex items-center gap-1.5\">\n <Show when={bound}>\n <Badge className=\"gap-1 text-[10px]\" size=\"xs\" variant=\"outline\">\n <ShapeGlyph\n className=\"size-2 shrink-0\"\n color={scale.color(Math.max(0, rank))}\n shape={scale.shape(Math.max(0, rank))}\n />\n {nameOf(category, options.categories)}\n </Badge>\n </Show>\n <Button\n className=\"h-5 gap-1 text-[10px]\"\n onClick={() => api.reveal(current.vertex)}\n size=\"sm\"\n title=\"Bring this vertex into view on the canvas\"\n variant=\"ghost\"\n >\n <CrosshairIcon className=\"size-3\" />\n Find on canvas\n </Button>\n </div>\n </div>\n <DataList orientation=\"vertical\">\n {current.fields.map((f) => (\n <DataListItem className=\"gap-0.5 py-0\" key={f.name}>\n <DataListItemLabel className=\"text-xs\">{f.name}</DataListItemLabel>\n <DataListItemValue className=\"break-all\">{text(f.value)}</DataListItemValue>\n </DataListItem>\n ))}\n {children?.(current)}\n </DataList>\n </>\n )}\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAmCA;AAeO;;AACL;;AAKoC;AAAqB;AAKzD;AACE;AACA;AACA;AAAoD;AACwB;AAExE;AAEsC;AACxC;AAEmB;AAGvB;;AAE4C;AAA8C;AAQ1F;AAEI;AAEA;AAGA;AAKA;AAGI;AACE;AAEA;AAEE;AAEI;AAAA;AAAC;AAAA;AACW;AAC0B;AACA;AAAA;AAAA;AAEF;AAExC;AACA;AAAC;AAAA;AACW;AAC8B;AACnC;AACC;AACE;AAER;AAAkC;AAAE;AAAA;AAAA;AAAA;AAGxC;AACF;AAEG;AAEG;AAA+C;AACS;AAE3D;AACW;AACd;AACF;AAIR;;;;"}
@@ -1,12 +1,13 @@
1
1
  export type GraphLegendProps = React.ComponentProps<"div">;
2
2
  /**
3
3
  * **The categorical scale the canvas draws, as rows** — glyph, name and how many vertices of that
4
- * category are drawn, then what is drawn of the whole. Unbound, the categories are the vertex types.
4
+ * category are drawn; what is drawn of the whole is `GraphCounts`. Unbound, the categories are the
5
+ * vertex types, and with no category at all there is nothing to key, so nothing is drawn.
5
6
  * The rows are the domain fixed before the graph loaded, so a row never moves and a colour never
6
7
  * changes hands; a category past the palette's capacity is Other, as it is on the canvas.
7
8
  *
8
9
  * Names are the root's `categories`, never this part's: a legend that renamed a category would be
9
10
  * encoding one product's policy, which is the parts rule's reversal.
10
11
  */
11
- export declare function GraphLegend({ className, slot, ...rest }: GraphLegendProps): import("react").JSX.Element;
12
+ export declare function GraphLegend({ className, slot, ...rest }: GraphLegendProps): import("react").JSX.Element | null;
12
13
  //# sourceMappingURL=graph-legend.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-legend.d.ts","sourceRoot":"","sources":["../../src/parts/graph-legend.tsx"],"names":[],"mappings":"AAUA,MAAM,MAAM,gBAAgB,GAAG,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;AAI3D;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,gBAAgB,+BA8CzE"}
1
+ {"version":3,"file":"graph-legend.d.ts","sourceRoot":"","sources":["../../src/parts/graph-legend.tsx"],"names":[],"mappings":"AAUA,MAAM,MAAM,gBAAgB,GAAG,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;AAI3D;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,gBAAgB,sCAqCzE"}