@kanzo-tech/graph 0.25.0 → 0.27.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.
- package/README.md +2 -2
- package/dist/core/categories.d.ts +1 -1
- package/dist/core/categories.d.ts.map +1 -1
- package/dist/core/categories.js.map +1 -1
- package/dist/core/channels.d.ts +12 -6
- package/dist/core/channels.d.ts.map +1 -1
- package/dist/core/channels.js +15 -12
- package/dist/core/channels.js.map +1 -1
- package/dist/core/client.d.ts +26 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +62 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/error.d.ts +3 -3
- package/dist/core/error.d.ts.map +1 -1
- package/dist/core/error.js.map +1 -1
- package/dist/core/load.d.ts +30 -39
- package/dist/core/load.d.ts.map +1 -1
- package/dist/core/load.js +63 -117
- package/dist/core/load.js.map +1 -1
- package/dist/core/source.d.ts +90 -0
- package/dist/core/source.d.ts.map +1 -0
- package/dist/core/source.js +105 -0
- package/dist/core/source.js.map +1 -0
- package/dist/core/state.d.ts +19 -20
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +138 -158
- package/dist/core/store.js.map +1 -1
- package/dist/core/types.d.ts +5 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -18
- package/dist/index.js.map +1 -1
- package/dist/parts/graph-canvas.d.ts.map +1 -1
- package/dist/parts/graph-canvas.js +49 -48
- package/dist/parts/graph-canvas.js.map +1 -1
- package/dist/parts/graph-counts.js +4 -4
- package/dist/parts/graph-counts.js.map +1 -1
- package/dist/parts/graph-inspector.d.ts +3 -3
- package/dist/parts/graph-inspector.d.ts.map +1 -1
- package/dist/parts/graph-inspector.js +45 -43
- package/dist/parts/graph-inspector.js.map +1 -1
- package/dist/parts/graph-search.d.ts +7 -7
- package/dist/parts/graph-search.d.ts.map +1 -1
- package/dist/parts/graph-search.js +60 -69
- package/dist/parts/graph-search.js.map +1 -1
- package/dist/parts/graph-select.d.ts +27 -0
- package/dist/parts/graph-select.d.ts.map +1 -0
- package/dist/parts/graph-select.js +53 -0
- package/dist/parts/graph-select.js.map +1 -0
- package/dist/parts/graph-toolbar.js +1 -1
- package/dist/parts/graph-toolbar.js.map +1 -1
- package/dist/react/use-graph.js +31 -31
- package/dist/react/use-graph.js.map +1 -1
- package/dist/render/camera.d.ts +12 -1
- package/dist/render/camera.d.ts.map +1 -1
- package/dist/render/camera.js +33 -21
- package/dist/render/camera.js.map +1 -1
- package/dist/render/graph-sim.d.ts +1 -1
- package/dist/render/graph-sim.d.ts.map +1 -1
- package/dist/render/graph-sim.js.map +1 -1
- package/dist/render/motion.d.ts +33 -0
- package/dist/render/motion.d.ts.map +1 -0
- package/dist/render/motion.js +19 -0
- package/dist/render/motion.js.map +1 -0
- package/dist/render/placement.d.ts +2 -4
- package/dist/render/placement.d.ts.map +1 -1
- package/dist/render/placement.js +12 -16
- package/dist/render/placement.js.map +1 -1
- package/dist/render/renderer.d.ts +6 -5
- package/dist/render/renderer.d.ts.map +1 -1
- package/dist/render/renderer.js +130 -118
- package/dist/render/renderer.js.map +1 -1
- package/dist/section.js +1 -1
- package/dist/section.js.map +1 -1
- package/package.json +12 -14
- package/dist/core/detail.d.ts +0 -26
- package/dist/core/detail.d.ts.map +0 -1
- package/dist/core/detail.js +0 -63
- package/dist/core/detail.js.map +0 -1
- package/dist/core/filter.d.ts +0 -23
- package/dist/core/filter.d.ts.map +0 -1
- package/dist/core/filter.js +0 -95
- package/dist/core/filter.js.map +0 -1
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs as
|
|
2
|
+
import { jsxs as x, jsx as a } from "react/jsx-runtime";
|
|
3
3
|
import { useThemeTick as Y, categoricalCapacity as Z, Show as f, cn as ee } from "@kanzo-tech/ui";
|
|
4
|
-
import { useMemo as
|
|
5
|
-
import { nameOf as
|
|
6
|
-
import { readTitles as
|
|
4
|
+
import { useMemo as b, useCallback as $, useRef as te, useEffect as y, useState as oe } from "react";
|
|
5
|
+
import { nameOf as re } from "../core/categories.js";
|
|
6
|
+
import { readTitles as ne } from "../core/source.js";
|
|
7
7
|
import { useGraphContext as D } from "../react/graph-root.js";
|
|
8
8
|
import { internalsOf as ae } from "../react/use-graph.js";
|
|
9
|
-
import { useGraphState as
|
|
9
|
+
import { useGraphState as u, useGraphSnapshot as R } from "../react/use-graph-state.js";
|
|
10
10
|
import { resolveLook as se } from "../render/graph-looks.js";
|
|
11
11
|
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
15
|
const F = "color-mix(in oklab, var(--primary) 17%, transparent)";
|
|
16
|
-
function
|
|
16
|
+
function ue(m, d, i, p, s) {
|
|
17
17
|
const c = d.sizes, o = [];
|
|
18
18
|
if (c && p > 0)
|
|
19
|
-
for (let t = 0; t <
|
|
19
|
+
for (let t = 0; t < m.size; t++) {
|
|
20
20
|
const n = c[t];
|
|
21
21
|
if (i && !i[t] || Number.isNaN(n) || o.length === p && n <= c[o[p - 1]]) continue;
|
|
22
22
|
let r = o.length;
|
|
@@ -25,65 +25,66 @@ function me(u, d, i, p, s) {
|
|
|
25
25
|
}
|
|
26
26
|
return s === null || o.includes(s) ? o : [s, ...o];
|
|
27
27
|
}
|
|
28
|
-
function E(
|
|
29
|
-
const d = D(), i =
|
|
28
|
+
function E(m) {
|
|
29
|
+
const d = D(), i = u((n) => n.structure), p = u((n) => n.options.title), s = u((n) => n.options.coordinator), [c, o] = oe(() => /* @__PURE__ */ new Map()), t = m.join(",");
|
|
30
30
|
return y(() => {
|
|
31
31
|
if (!i || !s || t === "") return;
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
(r) =>
|
|
35
|
-
(r) =>
|
|
36
|
-
), () =>
|
|
32
|
+
let n = !0;
|
|
33
|
+
return ne(s, i, t.split(",").map(Number), p).then(
|
|
34
|
+
(r) => n && o(r),
|
|
35
|
+
(r) => n && d.getState().options.onFailure(r)
|
|
36
|
+
), () => {
|
|
37
|
+
n = !1;
|
|
38
|
+
};
|
|
37
39
|
}, [d, i, s, t, p]), c;
|
|
38
40
|
}
|
|
39
|
-
const
|
|
40
|
-
opening: "Opening the corpus…",
|
|
41
|
+
const me = {
|
|
41
42
|
loading: "Loading the graph…"
|
|
42
43
|
};
|
|
43
|
-
function
|
|
44
|
-
const s = D(), { attach: c, renderer: o } = ae(s), t =
|
|
44
|
+
function ze({ children: m, className: d, slot: i, ...p }) {
|
|
45
|
+
const s = D(), { attach: c, renderer: o } = ae(s), t = u((e) => e.focus), n = u((e) => e.tool), r = u((e) => e.options), k = u((e) => {
|
|
45
46
|
var h;
|
|
46
47
|
return ((h = e.drawn) == null ? void 0 : h.domain) ?? e.domain;
|
|
47
|
-
}), w =
|
|
48
|
+
}), w = u((e) => e.drawn === null ? me[e.status] : void 0), g = b(() => se(r.look), [r.look]), _ = $(() => {
|
|
48
49
|
var e;
|
|
49
50
|
return ((e = o()) == null ? void 0 : e.graph) ?? null;
|
|
50
|
-
}, [o]), q = pe({ getGraph:
|
|
51
|
+
}, [o]), q = pe({ getGraph: _ }), { cardRef: B, gridRef: J, hostRef: S, hoverAt: O, labelRef: K, schedule: v, setHovered: P, setLabelOrder: j, track: I } = q, W = te(null), T = R((e) => e.geometry), N = R((e) => e.encoding), A = R((e) => e.mask);
|
|
51
52
|
y(() => {
|
|
52
|
-
const e =
|
|
53
|
+
const e = W.current;
|
|
53
54
|
if (e)
|
|
54
|
-
return c(e, { onFrame: v, onHover:
|
|
55
|
-
}, [c,
|
|
55
|
+
return c(e, { onFrame: v, onHover: O });
|
|
56
|
+
}, [c, O, v]);
|
|
56
57
|
const Q = Y();
|
|
57
58
|
y(() => {
|
|
58
59
|
var e;
|
|
59
60
|
(e = o()) == null || e.repaint();
|
|
60
61
|
}, [o, Q]);
|
|
61
|
-
const
|
|
62
|
-
() =>
|
|
63
|
-
[
|
|
64
|
-
), M = E(
|
|
65
|
-
() =>
|
|
66
|
-
[
|
|
62
|
+
const G = b(
|
|
63
|
+
() => T && N ? ue(T, N, A, g.labels, t) : [],
|
|
64
|
+
[T, N, A, t, g.labels]
|
|
65
|
+
), M = E(G), z = b(
|
|
66
|
+
() => G.flatMap((e) => M.get(e) ? [{ vertex: e, text: M.get(e) }] : []),
|
|
67
|
+
[G, M]
|
|
67
68
|
);
|
|
68
69
|
y(() => {
|
|
69
|
-
j(z.map((e) => e.vertex)),
|
|
70
|
-
}, [z, v, j,
|
|
70
|
+
j(z.map((e) => e.vertex)), I(), v();
|
|
71
|
+
}, [z, v, j, I, g.grid]);
|
|
71
72
|
const H = ie({
|
|
72
|
-
getGraph:
|
|
73
|
+
getGraph: _,
|
|
73
74
|
getSelection: $(() => s.getState().selection, [s]),
|
|
74
75
|
commit: (e, h, X) => s.select(e ? [...e] : null, h, X),
|
|
75
76
|
setTool: s.setTool,
|
|
76
77
|
tool: n
|
|
77
|
-
}), { active: U, drag: l, preview:
|
|
78
|
-
return /* @__PURE__ */
|
|
78
|
+
}), { active: U, drag: l, preview: C } = H, L = S.current ? Z(S.current) : void 0, V = b(() => le(r, L), [r, L]);
|
|
79
|
+
return /* @__PURE__ */ x(
|
|
79
80
|
"div",
|
|
80
81
|
{
|
|
81
82
|
...p,
|
|
82
83
|
className: ee("relative isolate size-full overflow-hidden bg-background", d),
|
|
83
84
|
"data-slot": i ?? "graph-canvas",
|
|
84
|
-
ref:
|
|
85
|
+
ref: S,
|
|
85
86
|
children: [
|
|
86
|
-
/* @__PURE__ */ a("div", { className: "size-full", "data-slot": "graph-canvas-surface", ref:
|
|
87
|
+
/* @__PURE__ */ a("div", { className: "size-full", "data-slot": "graph-canvas-surface", ref: W }),
|
|
87
88
|
/* @__PURE__ */ a(f, { when: g.grid, children: /* @__PURE__ */ a(
|
|
88
89
|
"div",
|
|
89
90
|
{
|
|
@@ -99,7 +100,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
99
100
|
style: { background: "radial-gradient(ellipse 75% 65% at 50% 45%, transparent 30%, var(--background) 100%)" }
|
|
100
101
|
}
|
|
101
102
|
) }),
|
|
102
|
-
/* @__PURE__ */
|
|
103
|
+
/* @__PURE__ */ x("div", { className: "pointer-events-none absolute inset-0 overflow-hidden", children: [
|
|
103
104
|
z.map((e) => /* @__PURE__ */ a(
|
|
104
105
|
"span",
|
|
105
106
|
{
|
|
@@ -122,7 +123,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
122
123
|
}
|
|
123
124
|
)
|
|
124
125
|
] }),
|
|
125
|
-
/* @__PURE__ */ a(f, { when: U !== null, children: /* @__PURE__ */
|
|
126
|
+
/* @__PURE__ */ a(f, { when: U !== null, children: /* @__PURE__ */ x("div", { className: "absolute inset-0 cursor-crosshair", "data-slot": "graph-canvas-gesture", ...H.handlers, children: [
|
|
126
127
|
l && /* @__PURE__ */ a("svg", { className: "pointer-events-none size-full", children: l.tool === "rect" ? /* @__PURE__ */ a(
|
|
127
128
|
"rect",
|
|
128
129
|
{
|
|
@@ -144,15 +145,15 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
144
145
|
style: { fill: F }
|
|
145
146
|
}
|
|
146
147
|
) }),
|
|
147
|
-
/* @__PURE__ */ a(f, { when:
|
|
148
|
+
/* @__PURE__ */ a(f, { when: C !== null, children: /* @__PURE__ */ x(
|
|
148
149
|
"span",
|
|
149
150
|
{
|
|
150
151
|
className: "pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums",
|
|
151
152
|
style: ce(l),
|
|
152
153
|
children: [
|
|
153
|
-
|
|
154
|
+
C,
|
|
154
155
|
" node",
|
|
155
|
-
|
|
156
|
+
C === 1 ? "" : "s"
|
|
156
157
|
]
|
|
157
158
|
}
|
|
158
159
|
) })
|
|
@@ -165,23 +166,23 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
165
166
|
children: w
|
|
166
167
|
}
|
|
167
168
|
) }),
|
|
168
|
-
|
|
169
|
+
m
|
|
169
170
|
]
|
|
170
171
|
}
|
|
171
172
|
);
|
|
172
173
|
}
|
|
173
|
-
function he({ cardRef:
|
|
174
|
-
const t =
|
|
174
|
+
function he({ cardRef: m, domain: d, encoding: i, options: p, scale: s, schedule: c, setHovered: o }) {
|
|
175
|
+
const t = u((w) => w.hovered), n = E(b(() => t === null ? [] : [t], [t]));
|
|
175
176
|
if (y(() => {
|
|
176
177
|
o(t), c();
|
|
177
178
|
}, [t, c, o, n]), t === null || !i || t >= i.ranks.length) return null;
|
|
178
|
-
const r = i.ranks[t] ?? 0, k = r < d.length ?
|
|
179
|
-
return /* @__PURE__ */
|
|
179
|
+
const r = i.ranks[t] ?? 0, k = r < d.length ? re(d[r], p.categories) : null;
|
|
180
|
+
return /* @__PURE__ */ x(
|
|
180
181
|
"div",
|
|
181
182
|
{
|
|
182
183
|
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",
|
|
183
184
|
"data-slot": "graph-canvas-card",
|
|
184
|
-
ref:
|
|
185
|
+
ref: m,
|
|
185
186
|
children: [
|
|
186
187
|
/* @__PURE__ */ a(de, { className: "size-2.5 shrink-0", color: s.color(r), shape: s.shape(r) }),
|
|
187
188
|
/* @__PURE__ */ a("span", { className: "truncate font-medium text-popover-foreground text-sm leading-none", children: n.get(t) || `#${t}` }),
|
|
@@ -191,6 +192,6 @@ function he({ cardRef: u, domain: d, encoding: i, options: p, scale: s, schedule
|
|
|
191
192
|
);
|
|
192
193
|
}
|
|
193
194
|
export {
|
|
194
|
-
|
|
195
|
+
ze as GraphCanvas
|
|
195
196
|
};
|
|
196
197
|
//# sourceMappingURL=graph-canvas.js.map
|
|
@@ -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),\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;;;;"}
|
|
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/source\";\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 structure = useGraphState((s) => s.structure);\n const title = useGraphState((s) => s.options.title);\n const coordinator = useGraphState((s) => s.options.coordinator);\n const [titles, setTitles] = useState<ReadonlyMap<VertexId, string>>(() => new Map());\n const key = vertices.join(\",\");\n useEffect(() => {\n if (!structure || !coordinator || key === \"\") return;\n let current = true;\n readTitles(coordinator, structure, key.split(\",\").map(Number), title).then(\n (found) => current && setTitles(found),\n (error: unknown) => current && api.getState().options.onFailure(error),\n );\n return () => {\n current = false;\n };\n }, [api, structure, coordinator, key, title]);\n return titles;\n}\n\nconst WAITING: Partial<Record<string, string>> = {\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;AAAsE;AAC/B;AACgC;AAGrE;AAAU;AACZ;AAGJ;AAEA;AAAiD;AAEjD;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;;;;"}
|
|
@@ -6,23 +6,23 @@ const v = { notation: "compact", maximumFractionDigits: 1 };
|
|
|
6
6
|
function r({ value: o }) {
|
|
7
7
|
return o == null ? "—" : /* @__PURE__ */ n(b, { value: o, ...v });
|
|
8
8
|
}
|
|
9
|
-
function N({ className: o, slot:
|
|
9
|
+
function N({ className: o, slot: c, spinner: u = !1, ...l }) {
|
|
10
10
|
const m = e((t) => {
|
|
11
11
|
var a;
|
|
12
12
|
return (a = t.drawn) == null ? void 0 : a.vertices;
|
|
13
13
|
}), d = e((t) => {
|
|
14
14
|
var a;
|
|
15
15
|
return (a = t.drawn) == null ? void 0 : a.edges;
|
|
16
|
-
}), g = e((t) => t.total), s = e((t) => t.status === "
|
|
16
|
+
}), g = e((t) => t.total), s = e((t) => t.status === "loading"), p = e((t) => t.motion === "running");
|
|
17
17
|
return /* @__PURE__ */ i(
|
|
18
18
|
"p",
|
|
19
19
|
{
|
|
20
20
|
"aria-busy": s || void 0,
|
|
21
21
|
...l,
|
|
22
22
|
className: x("flex items-center gap-1.5 text-muted-foreground text-xs tabular-nums", o),
|
|
23
|
-
"data-slot":
|
|
23
|
+
"data-slot": c ?? "graph-counts",
|
|
24
24
|
children: [
|
|
25
|
-
/* @__PURE__ */ n(f, { when:
|
|
25
|
+
/* @__PURE__ */ n(f, { when: u && (s || p), children: /* @__PURE__ */ n(h, { "aria-label": s ? "Loading" : "Laying out", className: "size-3" }) }),
|
|
26
26
|
/* @__PURE__ */ i("span", { children: [
|
|
27
27
|
/* @__PURE__ */ n(r, { value: m }),
|
|
28
28
|
" of ",
|
|
@@ -1 +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 === \"
|
|
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 === \"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,5 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { VertexDetail } from '../core/
|
|
2
|
+
import { VertexDetail } from '../core/source';
|
|
3
3
|
export interface GraphInspectorProps extends Omit<React.ComponentProps<"div">, "children"> {
|
|
4
4
|
/**
|
|
5
5
|
* More fields for the focused vertex, drawn inside its list after the corpus's own — a
|
|
@@ -11,8 +11,8 @@ export interface GraphInspectorProps extends Omit<React.ComponentProps<"div">, "
|
|
|
11
11
|
/**
|
|
12
12
|
* **The focused vertex, fetched and laid out by the corpus's own tables.** The loaded graph carries
|
|
13
13
|
* what the channels project and nothing else, so the rest of the row is read when a reader focuses
|
|
14
|
-
* it —
|
|
15
|
-
* table's, in the order the manifest declares them; the key
|
|
14
|
+
* it — one statement on its table, by its key, through the page's coordinator. The fields are the
|
|
15
|
+
* table's, in the order the manifest declares them; the key is the canvas's.
|
|
16
16
|
*/
|
|
17
17
|
export declare function GraphInspector({ children, className, slot, ...rest }: GraphInspectorProps): import("react").JSX.Element;
|
|
18
18
|
//# sourceMappingURL=graph-inspector.d.ts.map
|
|
@@ -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;AAYD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,mBAAmB,+
|
|
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,+BA4FzF"}
|
|
@@ -1,60 +1,62 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs as
|
|
3
|
-
import { useChartCapacity as B, Show as
|
|
2
|
+
import { jsxs as s, jsx as n, Fragment as T } from "react/jsx-runtime";
|
|
3
|
+
import { useChartCapacity as B, Show as p, Skeleton as F, Badge as M, Button as j, DataList as V, DataListItem as A, DataListItemLabel as E, DataListItemValue as $, cn as q } from "@kanzo-tech/ui";
|
|
4
4
|
import { CrosshairIcon as H } from "lucide-react";
|
|
5
5
|
import { useMemo as J, useState as K, useEffect as P } from "react";
|
|
6
6
|
import { nameOf as Q } from "../core/categories.js";
|
|
7
7
|
import { bindingOf as R } from "../core/channels.js";
|
|
8
|
-
import { readVertex as U } from "../core/
|
|
8
|
+
import { readVertex as U } from "../core/source.js";
|
|
9
9
|
import { useGraphContext as W } from "../react/graph-root.js";
|
|
10
|
-
import { useGraphState as
|
|
11
|
-
import { scaleOf as
|
|
12
|
-
import { ShapeGlyph as
|
|
13
|
-
const
|
|
14
|
-
function
|
|
10
|
+
import { useGraphState as c } 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 I = (a) => a instanceof Date ? a.toISOString().slice(0, 10) : a == null || a === "" ? "—" : String(a);
|
|
14
|
+
function ce({ children: a, className: k, slot: C, ...D }) {
|
|
15
15
|
var S;
|
|
16
|
-
const h = W(),
|
|
16
|
+
const h = W(), i = c((e) => e.focus), r = c((e) => e.structure), f = c((e) => e.options.coordinator), l = c((e) => e.options), L = c((e) => {
|
|
17
17
|
var o;
|
|
18
|
-
return ((o =
|
|
19
|
-
}), v = B(),
|
|
18
|
+
return ((o = e.drawn) == null ? void 0 : o.domain) ?? e.domain;
|
|
19
|
+
}), v = B(), N = J(() => X(l, v), [l, v]), [u, b] = K(null);
|
|
20
20
|
P(() => {
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
return U(
|
|
24
|
-
(o) =>
|
|
21
|
+
if (i === null || !r || !f) return;
|
|
22
|
+
let e = !0;
|
|
23
|
+
return U(f, r, i).then(
|
|
24
|
+
(o) => e && b({ vertex: i, detail: o }),
|
|
25
25
|
(o) => {
|
|
26
|
-
|
|
26
|
+
e && (b({ vertex: i, detail: !1 }), h.getState().options.onFailure(o));
|
|
27
27
|
}
|
|
28
|
-
), () =>
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
), () => {
|
|
29
|
+
e = !1;
|
|
30
|
+
};
|
|
31
|
+
}, [h, f, i, r]);
|
|
32
|
+
const m = u !== null && u.vertex === i ? u.detail : void 0, t = m || null, x = (e) => {
|
|
31
33
|
var o;
|
|
32
|
-
return (o =
|
|
33
|
-
},
|
|
34
|
-
return /* @__PURE__ */
|
|
35
|
-
/* @__PURE__ */ n(
|
|
36
|
-
/* @__PURE__ */ n(
|
|
37
|
-
/* @__PURE__ */ n(
|
|
38
|
-
|
|
39
|
-
/* @__PURE__ */
|
|
40
|
-
/* @__PURE__ */ n("p", { className: "truncate font-medium", "data-slot": "graph-inspector-title", children: g == null ? `#${
|
|
41
|
-
/* @__PURE__ */
|
|
42
|
-
/* @__PURE__ */ n(
|
|
34
|
+
return (o = t == null ? void 0 : t.fields.find((O) => O.name === e)) == null ? void 0 : o.value;
|
|
35
|
+
}, d = R(l), y = d.byTable ? t == null ? void 0 : t.table : x(d.category), w = L.findIndex((e) => String(e) === String(y)), z = d.byTable || d.category !== void 0, G = (S = r == null ? void 0 : r.vertices.find((e) => e.name === (t == null ? void 0 : t.table))) == null ? void 0 : S.identity, g = x(l.title) ?? x(G);
|
|
36
|
+
return /* @__PURE__ */ s("div", { ...D, className: q("space-y-3 text-sm", k), "data-slot": C ?? "graph-inspector", children: [
|
|
37
|
+
/* @__PURE__ */ n(p, { when: i === null, children: /* @__PURE__ */ n("p", { className: "text-muted-foreground text-xs", children: "Click a vertex on the canvas to inspect it." }) }),
|
|
38
|
+
/* @__PURE__ */ n(p, { when: i !== null && m === void 0, children: /* @__PURE__ */ n(F, { className: "h-24 w-full" }) }),
|
|
39
|
+
/* @__PURE__ */ n(p, { when: i !== null && m !== void 0 && !t, children: /* @__PURE__ */ n("p", { className: "text-muted-foreground text-xs", children: m === null ? "This vertex is not in the corpus." : "This vertex could not be read." }) }),
|
|
40
|
+
t && /* @__PURE__ */ s(T, { children: [
|
|
41
|
+
/* @__PURE__ */ s("div", { children: [
|
|
42
|
+
/* @__PURE__ */ n("p", { className: "truncate font-medium", "data-slot": "graph-inspector-title", children: g == null ? `#${t.vertex}` : I(g) }),
|
|
43
|
+
/* @__PURE__ */ s("div", { className: "mt-1 flex items-center gap-1.5", children: [
|
|
44
|
+
/* @__PURE__ */ n(p, { when: z, children: /* @__PURE__ */ s(M, { className: "gap-1 text-[10px]", size: "xs", variant: "outline", children: [
|
|
43
45
|
/* @__PURE__ */ n(
|
|
44
|
-
|
|
46
|
+
Y,
|
|
45
47
|
{
|
|
46
48
|
className: "size-2 shrink-0",
|
|
47
|
-
color:
|
|
48
|
-
shape:
|
|
49
|
+
color: N.color(Math.max(0, w)),
|
|
50
|
+
shape: N.shape(Math.max(0, w))
|
|
49
51
|
}
|
|
50
52
|
),
|
|
51
|
-
Q(
|
|
53
|
+
Q(y, l.categories)
|
|
52
54
|
] }) }),
|
|
53
|
-
/* @__PURE__ */
|
|
55
|
+
/* @__PURE__ */ s(
|
|
54
56
|
j,
|
|
55
57
|
{
|
|
56
58
|
className: "h-5 gap-1 text-[10px]",
|
|
57
|
-
onClick: () => h.reveal(
|
|
59
|
+
onClick: () => h.reveal(t.vertex),
|
|
58
60
|
size: "sm",
|
|
59
61
|
title: "Bring this vertex into view on the canvas",
|
|
60
62
|
variant: "ghost",
|
|
@@ -66,17 +68,17 @@ function mt({ children: a, className: I, slot: k, ...D }) {
|
|
|
66
68
|
)
|
|
67
69
|
] })
|
|
68
70
|
] }),
|
|
69
|
-
/* @__PURE__ */
|
|
70
|
-
|
|
71
|
-
/* @__PURE__ */ n(E, { className: "text-xs", children:
|
|
72
|
-
/* @__PURE__ */ n($, { className: "break-all", children:
|
|
73
|
-
] },
|
|
74
|
-
a == null ? void 0 : a(
|
|
71
|
+
/* @__PURE__ */ s(V, { orientation: "vertical", children: [
|
|
72
|
+
t.fields.map((e) => /* @__PURE__ */ s(A, { className: "gap-0.5 py-0", children: [
|
|
73
|
+
/* @__PURE__ */ n(E, { className: "text-xs", children: e.name }),
|
|
74
|
+
/* @__PURE__ */ n($, { className: "break-all", children: I(e.value) })
|
|
75
|
+
] }, e.name)),
|
|
76
|
+
a == null ? void 0 : a(t)
|
|
75
77
|
] })
|
|
76
78
|
] })
|
|
77
79
|
] });
|
|
78
80
|
}
|
|
79
81
|
export {
|
|
80
|
-
|
|
82
|
+
ce as GraphInspector
|
|
81
83
|
};
|
|
82
84
|
//# 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/
|
|
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/source\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { 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 — one statement on its table, by its key, through the page's coordinator. The fields are the\n * table's, in the order the manifest declares them; the key is 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 structure = useGraphState((s) => s.structure);\n const coordinator = useGraphState((s) => s.options.coordinator);\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 || !structure || !coordinator) return;\n let current = true;\n readVertex(coordinator, structure, focus).then(\n (detail) => current && setAnswer({ vertex: focus, detail }),\n (error: unknown) => {\n if (!current) return;\n setAnswer({ vertex: focus, detail: false });\n api.getState().options.onFailure(error);\n },\n );\n return () => {\n current = false;\n };\n }, [api, coordinator, focus, structure]);\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 = structure?.vertices.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":["current"],"mappings":";;;;;;;;;;;;AAmCA;AAeO;;AACL;;AAKoC;AAAqB;AAKzD;AACE;AACA;AACA;AAA0C;AACkB;AAExD;AAEsC;AACxC;AAGAA;AAAU;AACZ;AAGF;;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;;;;"}
|
|
@@ -4,14 +4,14 @@ export interface GraphSearchProps extends Pick<React.ComponentProps<typeof Combo
|
|
|
4
4
|
limit?: number;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* **Find a vertex by its text and go to it
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* **Find a vertex by its text and go to it** — Cosmograph's search: each pause in typing asks the
|
|
8
|
+
* corpus for the first `limit` vertices whose `title` — or its table's `identity` — contains the text,
|
|
9
|
+
* case-insensitively, one `ILIKE` over every vertex table through the page's coordinator. Nothing is
|
|
10
|
+
* read before the reader types, and no vertex's text is held in the page. Each match is named by the
|
|
11
|
+
* root's `categories`; picking one is `reveal`: the canvas frames it and selects it with its
|
|
12
|
+
* neighbours, and `GraphInspector` reads it.
|
|
12
13
|
*
|
|
13
|
-
* A
|
|
14
|
-
* A read that fails is handed to `onFailure` whole and the input says so, rather than staying disabled.
|
|
14
|
+
* A read that fails is handed to `onFailure` whole and the input says so.
|
|
15
15
|
*/
|
|
16
16
|
export declare function GraphSearch({ className, limit, placeholder, size }: GraphSearchProps): import("react").JSX.Element;
|
|
17
17
|
//# sourceMappingURL=graph-search.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-search.d.ts","sourceRoot":"","sources":["../../src/parts/graph-search.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,aAAa,
|
|
1
|
+
{"version":3,"file":"graph-search.d.ts","sourceRoot":"","sources":["../../src/parts/graph-search.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,aAAa,EAKd,MAAM,gBAAgB,CAAC;AASxB,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,aAAa,CAAC,EAAE,WAAW,GAAG,aAAa,GAAG,MAAM,CAAC;IAC9H,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAcD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,EAAE,SAAS,EAAE,KAAU,EAAE,WAA4B,EAAE,IAAW,EAAE,EAAE,gBAAgB,+BA+EjH"}
|