@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,81 +1,72 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs as
|
|
3
|
-
import {
|
|
4
|
-
import { SearchIcon as
|
|
5
|
-
import { useState as
|
|
6
|
-
import { nameOf as
|
|
7
|
-
import { bindingOf as
|
|
8
|
-
import {
|
|
9
|
-
import { useGraphContext as
|
|
10
|
-
import { useGraphState as
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const e = [];
|
|
43
|
-
for (const n of y ?? []) {
|
|
44
|
-
if (e.length === s) break;
|
|
45
|
-
(!b || k(n.label, b)) && e.push(n);
|
|
46
|
-
}
|
|
47
|
-
return A({ items: e });
|
|
48
|
-
}, [y, b, k, s]);
|
|
49
|
-
return /* @__PURE__ */ h(
|
|
50
|
-
O,
|
|
2
|
+
import { jsxs as i, jsx as n } from "react/jsx-runtime";
|
|
3
|
+
import { createListCollection as E, Combobox as V, ComboboxInput as j, InputGroupAddon as q, ComboboxContent as A, ComboboxEmpty as O, ComboboxItem as K, Show as w } from "@kanzo-tech/ui";
|
|
4
|
+
import { SearchIcon as L } from "lucide-react";
|
|
5
|
+
import { useState as k, useEffect as M, useMemo as P } from "react";
|
|
6
|
+
import { nameOf as Q } from "../core/categories.js";
|
|
7
|
+
import { bindingOf as U } from "../core/channels.js";
|
|
8
|
+
import { searchTitles as $ } from "../core/source.js";
|
|
9
|
+
import { useGraphContext as z } from "../react/graph-root.js";
|
|
10
|
+
import { useGraphState as f, useGraphSnapshot as B } from "../react/use-graph-state.js";
|
|
11
|
+
const D = 150;
|
|
12
|
+
function ne({ className: G, limit: r = 50, placeholder: I = "Find a node…", size: N = "sm" }) {
|
|
13
|
+
const l = z(), a = f((e) => e.structure), b = f((e) => e.options), c = B((e) => e.encoding), g = f((e) => {
|
|
14
|
+
var o;
|
|
15
|
+
return ((o = e.drawn) == null ? void 0 : o.domain) ?? e.domain;
|
|
16
|
+
}), [u, x] = k(null), [t, T] = k(""), { title: C, categories: y, coordinator: d } = b, S = U(b), v = S.byTable || S.category !== void 0;
|
|
17
|
+
M(() => {
|
|
18
|
+
if (!a || !d || t === "") return;
|
|
19
|
+
let e = !0;
|
|
20
|
+
const o = setTimeout(() => {
|
|
21
|
+
$(d, a, t, C, r).then(
|
|
22
|
+
(p) => e && x({
|
|
23
|
+
query: t,
|
|
24
|
+
entries: p.map(({ id: h, text: F }) => ({
|
|
25
|
+
value: String(h),
|
|
26
|
+
label: F || `#${h}`,
|
|
27
|
+
kind: v && c ? Q(g[c.ranks[h]], y) : null
|
|
28
|
+
}))
|
|
29
|
+
}),
|
|
30
|
+
(p) => {
|
|
31
|
+
e && (x({ query: t, entries: !1 }), l.getState().options.onFailure(p));
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
}, D);
|
|
35
|
+
return () => {
|
|
36
|
+
e = !1, clearTimeout(o);
|
|
37
|
+
};
|
|
38
|
+
}, [l, a, d, t, C, r, v, c, g, y]);
|
|
39
|
+
const s = u && u.query === t ? u.entries : null, m = P(() => E({ items: s || [] }), [s]);
|
|
40
|
+
return /* @__PURE__ */ i(
|
|
41
|
+
V,
|
|
51
42
|
{
|
|
52
|
-
collection:
|
|
53
|
-
disabled:
|
|
54
|
-
onInputValueChange: (e) =>
|
|
43
|
+
collection: m,
|
|
44
|
+
disabled: a === null,
|
|
45
|
+
onInputValueChange: (e) => T(e.inputValue),
|
|
55
46
|
onValueChange: (e) => {
|
|
56
|
-
const
|
|
57
|
-
|
|
47
|
+
const o = e.value[0];
|
|
48
|
+
o !== void 0 && l.reveal(Number(o));
|
|
58
49
|
},
|
|
59
50
|
children: [
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
-
|
|
51
|
+
/* @__PURE__ */ n(
|
|
52
|
+
j,
|
|
62
53
|
{
|
|
63
|
-
"aria-invalid":
|
|
64
|
-
className:
|
|
65
|
-
placeholder:
|
|
66
|
-
size:
|
|
67
|
-
children: /* @__PURE__ */
|
|
54
|
+
"aria-invalid": s === !1 || void 0,
|
|
55
|
+
className: G,
|
|
56
|
+
placeholder: s === !1 ? "The names could not be read." : I,
|
|
57
|
+
size: N,
|
|
58
|
+
children: /* @__PURE__ */ n(q, { align: "inline-start", children: /* @__PURE__ */ n(L, {}) })
|
|
68
59
|
}
|
|
69
60
|
),
|
|
70
|
-
/* @__PURE__ */
|
|
71
|
-
/* @__PURE__ */
|
|
72
|
-
|
|
73
|
-
/* @__PURE__ */
|
|
74
|
-
/* @__PURE__ */
|
|
61
|
+
/* @__PURE__ */ i(A, { children: [
|
|
62
|
+
/* @__PURE__ */ n(O, { children: "Nothing by that name." }),
|
|
63
|
+
m.items.map((e) => /* @__PURE__ */ i(K, { item: e, children: [
|
|
64
|
+
/* @__PURE__ */ n("span", { className: "min-w-0 truncate", children: e.label }),
|
|
65
|
+
/* @__PURE__ */ n(w, { when: e.kind !== null, children: /* @__PURE__ */ n("span", { className: "ms-auto ps-2 text-muted-foreground text-xs", children: e.kind }) })
|
|
75
66
|
] }, e.value)),
|
|
76
|
-
/* @__PURE__ */
|
|
67
|
+
/* @__PURE__ */ n(w, { when: m.items.length === r, children: /* @__PURE__ */ i("p", { className: "border-t px-2 py-1.5 text-muted-foreground text-xs", children: [
|
|
77
68
|
"First ",
|
|
78
|
-
|
|
69
|
+
r,
|
|
79
70
|
". Keep typing to narrow it."
|
|
80
71
|
] }) })
|
|
81
72
|
] })
|
|
@@ -84,6 +75,6 @@ function oe({ className: m, limit: s = 50, placeholder: d = "Find a node…", si
|
|
|
84
75
|
);
|
|
85
76
|
}
|
|
86
77
|
export {
|
|
87
|
-
|
|
78
|
+
ne as GraphSearch
|
|
88
79
|
};
|
|
89
80
|
//# sourceMappingURL=graph-search.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-search.js","sources":["../../src/parts/graph-search.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Combobox,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n createListCollection,\n InputGroupAddon,\n Show,\n
|
|
1
|
+
{"version":3,"file":"graph-search.js","sources":["../../src/parts/graph-search.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Combobox,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n createListCollection,\n InputGroupAddon,\n Show,\n} from \"@kanzo-tech/ui\";\nimport { SearchIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { bindingOf } from \"../core/channels\";\nimport { searchTitles } from \"../core/source\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\n\nexport interface GraphSearchProps extends Pick<React.ComponentProps<typeof ComboboxInput>, \"className\" | \"placeholder\" | \"size\"> {\n /** How many matches the list shows; past it, the list says to keep typing. */\n limit?: number;\n}\n\ninterface Entry {\n value: string;\n label: string;\n kind: string | null;\n}\n\n/** What one query answered, or `false` when the read failed. */\ntype Found = { query: string; entries: Entry[] | false } | null;\n\n/** How long a reader pauses before the text is asked for: one statement per pause, not per key. */\nconst PAUSE = 150;\n\n/**\n * **Find a vertex by its text and go to it** — Cosmograph's search: each pause in typing asks the\n * corpus for the first `limit` vertices whose `title` — or its table's `identity` — contains the text,\n * case-insensitively, one `ILIKE` over every vertex table through the page's coordinator. Nothing is\n * read before the reader types, and no vertex's text is held in the page. Each match is named by the\n * root's `categories`; picking one is `reveal`: the canvas frames it and selects it with its\n * neighbours, and `GraphInspector` reads it.\n *\n * A read that fails is handed to `onFailure` whole and the input says so.\n */\nexport function GraphSearch({ className, limit = 50, placeholder = \"Find a node…\", size = \"sm\" }: GraphSearchProps) {\n const api = useGraphContext();\n const structure = useGraphState((s) => s.structure);\n const options = useGraphState((s) => s.options);\n const encoding = useGraphSnapshot((s) => s.encoding);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const [found, setFound] = useState<Found>(null);\n const [query, setQuery] = useState(\"\");\n const { title, categories, coordinator } = options;\n const binding = bindingOf(options);\n const bound = binding.byTable || binding.category !== undefined;\n\n useEffect(() => {\n if (!structure || !coordinator || query === \"\") return;\n let current = true;\n const timer = setTimeout(() => {\n searchTitles(coordinator, structure, query, title, limit).then(\n (rows) =>\n current &&\n setFound({\n query,\n entries: rows.map(({ id, text }) => ({\n value: String(id),\n label: text || `#${id}`,\n kind: bound && encoding ? nameOf(domain[encoding.ranks[id] as number], categories) : null,\n })),\n }),\n (error: unknown) => {\n if (!current) return;\n setFound({ query, entries: false });\n api.getState().options.onFailure(error);\n },\n );\n }, PAUSE);\n return () => {\n current = false;\n clearTimeout(timer);\n };\n }, [api, structure, coordinator, query, title, limit, bound, encoding, domain, categories]);\n\n const answered = found && found.query === query ? found.entries : null;\n const collection = useMemo(() => createListCollection({ items: answered || [] }), [answered]);\n\n return (\n <Combobox\n collection={collection}\n disabled={structure === null}\n onInputValueChange={(details) => setQuery(details.inputValue)}\n onValueChange={(details) => {\n const picked = details.value[0];\n if (picked !== undefined) api.reveal(Number(picked));\n }}\n >\n <ComboboxInput\n aria-invalid={answered === false || undefined}\n className={className}\n placeholder={answered === false ? \"The names could not be read.\" : placeholder}\n size={size}\n >\n <InputGroupAddon align=\"inline-start\">\n <SearchIcon />\n </InputGroupAddon>\n </ComboboxInput>\n <ComboboxContent>\n <ComboboxEmpty>Nothing by that name.</ComboboxEmpty>\n {collection.items.map((item) => (\n <ComboboxItem item={item} key={item.value}>\n <span className=\"min-w-0 truncate\">{item.label}</span>\n <Show when={item.kind !== null}>\n <span className=\"ms-auto ps-2 text-muted-foreground text-xs\">{item.kind}</span>\n </Show>\n </ComboboxItem>\n ))}\n <Show when={collection.items.length === limit}>\n <p className=\"border-t px-2 py-1.5 text-muted-foreground text-xs\">First {limit}. Keep typing to narrow it.</p>\n </Show>\n </ComboboxContent>\n </Combobox>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;AAmCA;AAYO;AACL;;AAIoC;AAAqB;AAOzD;AACE;AACA;AACA;AACE;AAA0D;AAG7C;AACP;AACqC;AACnB;AACK;AACgE;AACrF;AACH;AAED;AAEsC;AACxC;AAAA;AAGJ;AACE;AACkB;AACpB;AAGF;AAGA;AACE;AAAC;AAAA;AACC;AACwB;AACoC;AAE1D;AACA;AAAmD;AACrD;AAEA;AAAA;AAAC;AAAA;AACqC;AACpC;AACmE;AACnE;AAIA;AAAA;AAAA;AAGA;AAAoC;AAGhC;AAA+C;AAG/C;AAEH;AAEmE;AAAA;AAAO;AAAM;AACjF;AACF;AAAA;AAAA;AAGN;;;;"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { VertexId } from '../core/types';
|
|
2
|
+
export interface GraphSelectProps extends Omit<React.ComponentProps<"button">, "onClick" | "type"> {
|
|
3
|
+
/**
|
|
4
|
+
* The vertices this control selects — `dense_id`s, as SQL returns them. Called when the reader
|
|
5
|
+
* presses it and not before, so a panel of fifty findings reads nothing until one is asked for.
|
|
6
|
+
*/
|
|
7
|
+
load: () => Promise<readonly VertexId[]>;
|
|
8
|
+
/** What the corner calls the selection, and its identity: keep it distinct among a panel's controls. */
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* **A selection the host offers, as a toggle** — *these vertices, on the canvas*. Pressing it calls
|
|
13
|
+
* `load` and selects what it answers as an `"external"` selection named `label`; pressing it again
|
|
14
|
+
* clears it. Whatever the host renders inside is the clickable body: a rule and its count, an answer.
|
|
15
|
+
*
|
|
16
|
+
* The pressed state is **derived** from the live selection — `source === "external"` and the same
|
|
17
|
+
* `label` — rather than stored beside it. A stored flag goes stale the moment the reader lassoes
|
|
18
|
+
* something else; a selection cannot disagree with itself. It is disabled while `load` runs.
|
|
19
|
+
*
|
|
20
|
+
* **A rejected `load` reaches the root's `onFailure`, as thrown** — as `GraphSearch`'s failed read
|
|
21
|
+
* does — and the control stays unpressed, ready to be pressed again. It takes no handler of its own:
|
|
22
|
+
* what a failure shows is the host's policy, and the root is where that lives.
|
|
23
|
+
*
|
|
24
|
+
* ARIA: a `button` with `aria-pressed`, the toggle-button pattern; its name is its content.
|
|
25
|
+
*/
|
|
26
|
+
export declare function GraphSelect({ children, className, disabled, label, load, slot, ...rest }: GraphSelectProps): import("react").JSX.Element;
|
|
27
|
+
//# sourceMappingURL=graph-select.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-select.d.ts","sourceRoot":"","sources":["../../src/parts/graph-select.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAI9C,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAChG;;;OAGG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;IACzC,wGAAwG;IACxG,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,gBAAgB,+BA4C1G"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as b } from "react/jsx-runtime";
|
|
3
|
+
import { useState as f } from "react";
|
|
4
|
+
import { cn as y } from "@kanzo-tech/ui";
|
|
5
|
+
import { useGraphContext as g } from "../react/graph-root.js";
|
|
6
|
+
import { useGraphState as h } from "../react/use-graph-state.js";
|
|
7
|
+
function C({ children: n, className: l, disabled: i, label: o, load: c, slot: d, ...p }) {
|
|
8
|
+
const t = g(), r = h((e) => {
|
|
9
|
+
var s;
|
|
10
|
+
return ((s = e.selection) == null ? void 0 : s.source) === "external" && e.selection.label === o;
|
|
11
|
+
}), [u, a] = f(!1), m = async () => {
|
|
12
|
+
if (r) {
|
|
13
|
+
t.select(null);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
a(!0);
|
|
17
|
+
try {
|
|
18
|
+
t.select(await c(), "external", o);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
t.getState().options.onFailure(e);
|
|
21
|
+
} finally {
|
|
22
|
+
a(!1);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return /* @__PURE__ */ b(
|
|
26
|
+
"button",
|
|
27
|
+
{
|
|
28
|
+
"aria-pressed": r,
|
|
29
|
+
...p,
|
|
30
|
+
className: y(
|
|
31
|
+
"w-full rounded-md border p-2 text-start transition-colors",
|
|
32
|
+
"disabled:cursor-default disabled:opacity-60",
|
|
33
|
+
// Normal / hover / pressed is the ramp's own 3→4→5 progression, written with its names.
|
|
34
|
+
// Over the card, `bg-accent/50` and `bg-accent/60` composite to ΔE 1.21 (light) / 0.86
|
|
35
|
+
// (dark) of each other — under the ΔE 2 the ramp makes a hover owe, so the fill was not
|
|
36
|
+
// carrying the pressed state at all; `--secondary` → `--accent` measures 3.96 / 4.61.
|
|
37
|
+
// `border-primary/50` measures 1.73–2.32:1 for a hued seed, the failure `stroke-primary/50`
|
|
38
|
+
// had on the canvas; solid measures 3.07–6.07 (measured 2026-09, in the workspace showcase).
|
|
39
|
+
r ? "border-primary bg-accent" : "hover:bg-secondary disabled:hover:bg-transparent",
|
|
40
|
+
l
|
|
41
|
+
),
|
|
42
|
+
"data-slot": d ?? "graph-select",
|
|
43
|
+
disabled: i || u,
|
|
44
|
+
onClick: () => void m(),
|
|
45
|
+
type: "button",
|
|
46
|
+
children: n
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
C as GraphSelect
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=graph-select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-select.js","sources":["../../src/parts/graph-select.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"@kanzo-tech/ui\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphState } from \"../react/use-graph-state\";\n\nexport interface GraphSelectProps extends Omit<React.ComponentProps<\"button\">, \"onClick\" | \"type\"> {\n /**\n * The vertices this control selects — `dense_id`s, as SQL returns them. Called when the reader\n * presses it and not before, so a panel of fifty findings reads nothing until one is asked for.\n */\n load: () => Promise<readonly VertexId[]>;\n /** What the corner calls the selection, and its identity: keep it distinct among a panel's controls. */\n label: string;\n}\n\n/**\n * **A selection the host offers, as a toggle** — *these vertices, on the canvas*. Pressing it calls\n * `load` and selects what it answers as an `\"external\"` selection named `label`; pressing it again\n * clears it. Whatever the host renders inside is the clickable body: a rule and its count, an answer.\n *\n * The pressed state is **derived** from the live selection — `source === \"external\"` and the same\n * `label` — rather than stored beside it. A stored flag goes stale the moment the reader lassoes\n * something else; a selection cannot disagree with itself. It is disabled while `load` runs.\n *\n * **A rejected `load` reaches the root's `onFailure`, as thrown** — as `GraphSearch`'s failed read\n * does — and the control stays unpressed, ready to be pressed again. It takes no handler of its own:\n * what a failure shows is the host's policy, and the root is where that lives.\n *\n * ARIA: a `button` with `aria-pressed`, the toggle-button pattern; its name is its content.\n */\nexport function GraphSelect({ children, className, disabled, label, load, slot, ...rest }: GraphSelectProps) {\n const api = useGraphContext();\n const pressed = useGraphState((s) => s.selection?.source === \"external\" && s.selection.label === label);\n const [busy, setBusy] = useState(false);\n\n const toggle = async () => {\n if (pressed) {\n api.select(null);\n return;\n }\n setBusy(true);\n try {\n api.select(await load(), \"external\", label);\n } catch (error) {\n api.getState().options.onFailure(error);\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <button\n aria-pressed={pressed}\n {...rest}\n className={cn(\n \"w-full rounded-md border p-2 text-start transition-colors\",\n \"disabled:cursor-default disabled:opacity-60\",\n // Normal / hover / pressed is the ramp's own 3→4→5 progression, written with its names.\n // Over the card, `bg-accent/50` and `bg-accent/60` composite to ΔE 1.21 (light) / 0.86\n // (dark) of each other — under the ΔE 2 the ramp makes a hover owe, so the fill was not\n // carrying the pressed state at all; `--secondary` → `--accent` measures 3.96 / 4.61.\n // `border-primary/50` measures 1.73–2.32:1 for a hued seed, the failure `stroke-primary/50`\n // had on the canvas; solid measures 3.07–6.07 (measured 2026-09, in the workspace showcase).\n pressed ? \"border-primary bg-accent\" : \"hover:bg-secondary disabled:hover:bg-transparent\",\n className,\n )}\n data-slot={slot ?? \"graph-select\"}\n disabled={disabled || busy}\n onClick={() => void toggle()}\n type=\"button\"\n >\n {children}\n </button>\n );\n}\n"],"names":[],"mappings":";;;;;;AAiCO;AACL;;AACqC;AAA4D;AAI/F;AACE;AACA;AAAA;AAEF;AACA;AACE;AAA0C;AAE1C;AAAsC;AAEtC;AAAa;AACf;AAGF;AACE;AAAC;AAAA;AACe;AACV;AACO;AACT;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOuC;AACvC;AAAA;AAEiB;AACG;AACF;AACf;AAEJ;AAAA;AAGP;;;;"}
|
|
@@ -10,7 +10,7 @@ const m = "rounded-lg border bg-card shadow-sm", v = 1.4, j = {
|
|
|
10
10
|
settled: { pause: !1, label: "Run the layout" }
|
|
11
11
|
};
|
|
12
12
|
function E({ className: z, orientation: c = "horizontal", slot: x, ...S }) {
|
|
13
|
-
const a = Z(), t = r((l) => l.
|
|
13
|
+
const a = Z(), t = r((l) => l.structure !== null), n = r((l) => l.tool), d = r((l) => l.selection), g = r((l) => l.total), u = r((l) => l.motion), p = r((l) => l.pinned.length), b = j[u], f = `Release ${p} pinned ${p === 1 ? "node" : "nodes"}`;
|
|
14
14
|
return /* @__PURE__ */ s(
|
|
15
15
|
"div",
|
|
16
16
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-toolbar.js","sources":["../../src/parts/graph-toolbar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText, cn, Show } from \"@kanzo-tech/ui\";\nimport {\n LassoIcon,\n MaximizeIcon,\n MinusIcon,\n PauseIcon,\n PinOffIcon,\n PlayIcon,\n PlusIcon,\n RotateCcwIcon,\n ScanIcon,\n SquareDashedIcon,\n XIcon,\n} from \"lucide-react\";\nimport type { Motion } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphState } from \"../react/use-graph-state\";\n\nexport interface GraphToolbarProps extends React.ComponentProps<\"div\"> {\n /** How the clusters stack. Each cluster is a `ButtonGroup` in the same orientation. */\n orientation?: \"horizontal\" | \"vertical\";\n}\n\n/**\n * Solid, not `bg-card/85 backdrop-blur-md`: a diluted surface is a function of whatever the layout\n * put behind it — measured over the plane and the eight slots, `card/85` spans ΔE 9.4–11.1, against\n * the ramp's own `interchangeable` bound of 4. The buttons are `ghost` because the group carries the\n * surface, so the divisions come back as explicit separators.\n */\nconst CLUSTER = \"rounded-lg border bg-card shadow-sm\";\n\nconst ZOOM = 1.4;\n\n/** One action per state, named for what it does: a settled layout is not paused, so it is woken. */\nconst TRANSPORT: Record<Motion, { pause: boolean; label: string }> = {\n running: { pause: true, label: \"Pause the layout\" },\n paused: { pause: false, label: \"Resume the layout\" },\n settled: { pause: false, label: \"Run the layout\" },\n};\n\n/**\n * **The commands, drawn** — the selection tools, the selection and what to do with it, the camera, and\n * the live layout's transport, which starts a layout from the points where they are. Each cluster is a `ButtonGroup`, because a cluster\n * is a claim that its buttons do one job; the part declares no `toolbar` role, since a `role` of that\n * kind promises roving focus and each button here is its own tab stop.\n *\n * The tools are not a `SegmentGroup`: that always has one member selected, and no tool — drag pans —\n * is the posture a reader spends most of their time in.\n */\nexport function GraphToolbar({ className, orientation = \"horizontal\", slot, ...rest }: GraphToolbarProps) {\n const api = useGraphContext();\n const ready = useGraphState((s) => s.
|
|
1
|
+
{"version":3,"file":"graph-toolbar.js","sources":["../../src/parts/graph-toolbar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText, cn, Show } from \"@kanzo-tech/ui\";\nimport {\n LassoIcon,\n MaximizeIcon,\n MinusIcon,\n PauseIcon,\n PinOffIcon,\n PlayIcon,\n PlusIcon,\n RotateCcwIcon,\n ScanIcon,\n SquareDashedIcon,\n XIcon,\n} from \"lucide-react\";\nimport type { Motion } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphState } from \"../react/use-graph-state\";\n\nexport interface GraphToolbarProps extends React.ComponentProps<\"div\"> {\n /** How the clusters stack. Each cluster is a `ButtonGroup` in the same orientation. */\n orientation?: \"horizontal\" | \"vertical\";\n}\n\n/**\n * Solid, not `bg-card/85 backdrop-blur-md`: a diluted surface is a function of whatever the layout\n * put behind it — measured over the plane and the eight slots, `card/85` spans ΔE 9.4–11.1, against\n * the ramp's own `interchangeable` bound of 4. The buttons are `ghost` because the group carries the\n * surface, so the divisions come back as explicit separators.\n */\nconst CLUSTER = \"rounded-lg border bg-card shadow-sm\";\n\nconst ZOOM = 1.4;\n\n/** One action per state, named for what it does: a settled layout is not paused, so it is woken. */\nconst TRANSPORT: Record<Motion, { pause: boolean; label: string }> = {\n running: { pause: true, label: \"Pause the layout\" },\n paused: { pause: false, label: \"Resume the layout\" },\n settled: { pause: false, label: \"Run the layout\" },\n};\n\n/**\n * **The commands, drawn** — the selection tools, the selection and what to do with it, the camera, and\n * the live layout's transport, which starts a layout from the points where they are. Each cluster is a `ButtonGroup`, because a cluster\n * is a claim that its buttons do one job; the part declares no `toolbar` role, since a `role` of that\n * kind promises roving focus and each button here is its own tab stop.\n *\n * The tools are not a `SegmentGroup`: that always has one member selected, and no tool — drag pans —\n * is the posture a reader spends most of their time in.\n */\nexport function GraphToolbar({ className, orientation = \"horizontal\", slot, ...rest }: GraphToolbarProps) {\n const api = useGraphContext();\n const ready = useGraphState((s) => s.structure !== null);\n const tool = useGraphState((s) => s.tool);\n const selection = useGraphState((s) => s.selection);\n const total = useGraphState((s) => s.total);\n const motion = useGraphState((s) => s.motion);\n const pinned = useGraphState((s) => s.pinned.length);\n const transport = TRANSPORT[motion];\n const release = `Release ${pinned} pinned ${pinned === 1 ? \"node\" : \"nodes\"}`;\n\n return (\n <div\n {...rest}\n className={cn(\"flex items-start gap-1.5\", orientation === \"vertical\" && \"flex-col items-end\", className)}\n data-slot={slot ?? \"graph-toolbar\"}\n >\n <ButtonGroup aria-label=\"Selection tool\" className={CLUSTER} orientation={orientation}>\n <Button\n aria-label=\"Marquee\"\n aria-pressed={tool === \"rect\"}\n disabled={!ready}\n onClick={() => api.setTool(tool === \"rect\" ? null : \"rect\")}\n size=\"icon-sm\"\n title=\"Drag a box — or just hold Shift\"\n variant={tool === \"rect\" ? \"default\" : \"ghost\"}\n >\n <SquareDashedIcon />\n </Button>\n <ButtonGroupSeparator />\n <Button\n aria-label=\"Lasso\"\n aria-pressed={tool === \"lasso\"}\n disabled={!ready}\n onClick={() => api.setTool(tool === \"lasso\" ? null : \"lasso\")}\n size=\"icon-sm\"\n title=\"Draw a loop around a cluster\"\n variant={tool === \"lasso\" ? \"default\" : \"ghost\"}\n >\n <LassoIcon />\n </Button>\n </ButtonGroup>\n\n <Show when={selection !== null}>\n <ButtonGroup aria-label=\"Current selection\" className={CLUSTER} data-slot=\"graph-toolbar-selection\">\n <ButtonGroupText className=\"gap-1 ps-2 pe-2 text-xs tabular-nums\">\n <span className=\"font-medium text-foreground\">{selection?.vertices.length.toLocaleString()}</span>\n {total === undefined ? \" selected\" : ` of ${total.toLocaleString()} selected`}\n </ButtonGroupText>\n <ButtonGroupSeparator />\n <Button aria-label=\"Frame the selection\" onClick={() => api.frameSelection()} size=\"icon-sm\" title=\"Frame the selection\" variant=\"ghost\">\n <ScanIcon />\n </Button>\n <Button aria-label=\"Clear the selection\" onClick={() => api.clear()} size=\"icon-sm\" title=\"Clear the selection\" variant=\"ghost\">\n <XIcon />\n </Button>\n </ButtonGroup>\n </Show>\n\n <ButtonGroup aria-label=\"Zoom and fit\" className={CLUSTER} orientation={orientation}>\n <Button aria-label=\"Zoom in\" disabled={!ready} onClick={() => api.zoomBy(ZOOM)} size=\"icon-sm\" variant=\"ghost\">\n <PlusIcon />\n </Button>\n <ButtonGroupSeparator />\n <Button aria-label=\"Zoom out\" disabled={!ready} onClick={() => api.zoomBy(1 / ZOOM)} size=\"icon-sm\" variant=\"ghost\">\n <MinusIcon />\n </Button>\n <ButtonGroupSeparator />\n <Button aria-label=\"Fit to view\" disabled={!ready} onClick={() => api.fit()} size=\"icon-sm\" variant=\"ghost\">\n <MaximizeIcon />\n </Button>\n </ButtonGroup>\n\n <ButtonGroup aria-label=\"Layout\" className={CLUSTER} orientation={orientation}>\n <Button\n aria-label={transport.label}\n aria-pressed={motion === \"paused\"}\n disabled={!ready}\n onClick={() => (transport.pause ? api.pause() : api.resume())}\n size=\"icon-sm\"\n title={transport.label}\n variant={motion === \"paused\" ? \"default\" : \"ghost\"}\n >\n {motion === \"running\" ? <PauseIcon /> : <PlayIcon />}\n </Button>\n <ButtonGroupSeparator />\n <Button aria-label=\"Re-run the layout\" disabled={!ready} onClick={() => api.restart()} size=\"icon-sm\" title=\"Re-run the layout\" variant=\"ghost\">\n <RotateCcwIcon />\n </Button>\n <Show when={pinned > 0}>\n <ButtonGroupSeparator />\n <Button aria-label={release} onClick={() => api.unpin()} size=\"icon-sm\" title={release} variant=\"ghost\">\n <PinOffIcon />\n </Button>\n </Show>\n </ButtonGroup>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;AA+BA;AAKqE;AACpC;AACA;AAEjC;AAWO;AACL;AAUA;AACE;AAAC;AAAA;AACK;AACmG;AACpF;AAEnB;AACE;AAAA;AAAC;AAAA;AACY;AACY;AACZ;AAC+C;AACrD;AACC;AACiC;AAErB;AAAA;AAAA;AAEE;AACtB;AAAC;AAAA;AACY;AACY;AACZ;AACiD;AACvD;AACC;AACkC;AAE7B;AAAA;AAAA;AAEf;AAII;AACE;AAA2F;AACzB;AACpE;AACsB;AAGtB;AAGA;AAEJ;AAGE;AAEA;AACsB;AAGtB;AACsB;AAGtB;AACF;AAGE;AAAA;AAAC;AAAA;AACuB;AACG;AACd;AACyC;AAC/C;AACY;AAC0B;AAEO;AAAA;AAAA;AAE9B;AAGtB;AAEE;AAAsB;AAGtB;AACF;AACF;AAAA;AAAA;AAGN;;;;"}
|
package/dist/react/use-graph.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useRef as
|
|
3
|
-
import { createGraph as
|
|
4
|
-
import { createRenderer as
|
|
2
|
+
import { useRef as N, useCallback as R, useState as T, useEffect as f } from "react";
|
|
3
|
+
import { createGraph as z } from "../core/store.js";
|
|
4
|
+
import { createRenderer as O } from "../render/renderer.js";
|
|
5
5
|
const m = /* @__PURE__ */ new WeakMap();
|
|
6
|
-
function
|
|
6
|
+
function v(e) {
|
|
7
7
|
const t = m.get(e);
|
|
8
8
|
if (!t) throw new Error("a graph part was given an api useGraph did not build");
|
|
9
9
|
return t;
|
|
10
10
|
}
|
|
11
|
-
function
|
|
12
|
-
const t =
|
|
11
|
+
function M(e) {
|
|
12
|
+
const t = N(e);
|
|
13
13
|
t.current = e;
|
|
14
|
-
const
|
|
15
|
-
(
|
|
16
|
-
...
|
|
14
|
+
const o = R(
|
|
15
|
+
(G) => ({
|
|
16
|
+
...G,
|
|
17
17
|
onFailure: (l) => t.current.onFailure(l),
|
|
18
18
|
onSelect: (l) => {
|
|
19
19
|
var a, s;
|
|
@@ -25,47 +25,47 @@ function A(e) {
|
|
|
25
25
|
}
|
|
26
26
|
}),
|
|
27
27
|
[]
|
|
28
|
-
), [
|
|
28
|
+
), [c] = T(() => A(z(o(e)))), { store: n } = v(c), { categories: u, cluster: r, coordinator: i, fill: p, filterBy: b, from: d, look: h, r: S, sim: y, simulate: g, stroke: w, symbol: F, title: k, x: B, y: E } = e;
|
|
29
29
|
return f(() => {
|
|
30
|
-
n.setOptions(
|
|
31
|
-
}, [n,
|
|
32
|
-
}), [n]),
|
|
30
|
+
n.setOptions(o(t.current));
|
|
31
|
+
}, [n, o, u, r, i, p, b, d, h, S, y, g, w, F, k, B, E]), f(() => n.subscribe(() => {
|
|
32
|
+
}), [n]), c;
|
|
33
33
|
}
|
|
34
|
-
function
|
|
34
|
+
function A(e) {
|
|
35
35
|
let t = null;
|
|
36
|
-
const
|
|
36
|
+
const o = (n) => (...u) => {
|
|
37
37
|
var r;
|
|
38
|
-
return (r = t == null ? void 0 : t[n]) == null ? void 0 : r.call(t, ...
|
|
39
|
-
},
|
|
40
|
-
zoomBy:
|
|
41
|
-
fit:
|
|
42
|
-
pause:
|
|
43
|
-
resume:
|
|
44
|
-
restart:
|
|
45
|
-
unpin:
|
|
38
|
+
return (r = t == null ? void 0 : t[n]) == null ? void 0 : r.call(t, ...u);
|
|
39
|
+
}, c = {
|
|
40
|
+
zoomBy: o("zoomBy"),
|
|
41
|
+
fit: o("fit"),
|
|
42
|
+
pause: o("pause"),
|
|
43
|
+
resume: o("resume"),
|
|
44
|
+
restart: o("restart"),
|
|
45
|
+
unpin: o("unpin"),
|
|
46
46
|
// Selecting and focusing are state, and hold without a renderer; centring is the camera's.
|
|
47
47
|
reveal: (n) => {
|
|
48
48
|
if (t) return t.reveal(n);
|
|
49
49
|
e.select([n], "node", "Node"), e.focus(n);
|
|
50
50
|
},
|
|
51
|
-
frameSelection:
|
|
51
|
+
frameSelection: o("frameSelection"),
|
|
52
52
|
clear: () => {
|
|
53
53
|
e.select(null), e.focus(null);
|
|
54
54
|
},
|
|
55
|
-
select: (n,
|
|
55
|
+
select: (n, u, r) => e.select(n, u, r),
|
|
56
56
|
setFocus: (n) => e.focus(n),
|
|
57
57
|
setTool: (n) => e.setTool(n),
|
|
58
58
|
subscribe: (n) => e.subscribe(n),
|
|
59
59
|
getState: () => e.getSnapshot()
|
|
60
60
|
};
|
|
61
|
-
return m.set(
|
|
61
|
+
return m.set(c, {
|
|
62
62
|
store: e,
|
|
63
63
|
renderer: () => t,
|
|
64
|
-
attach(n,
|
|
64
|
+
attach(n, u) {
|
|
65
65
|
e.renderable();
|
|
66
66
|
let r = null;
|
|
67
67
|
try {
|
|
68
|
-
r =
|
|
68
|
+
r = O(n, e, u);
|
|
69
69
|
} catch (i) {
|
|
70
70
|
e.unrenderable(i);
|
|
71
71
|
}
|
|
@@ -73,10 +73,10 @@ function T(e) {
|
|
|
73
73
|
r == null || r.destroy(), t === r && (t = null);
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
-
}),
|
|
76
|
+
}), c;
|
|
77
77
|
}
|
|
78
78
|
export {
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
v as internalsOf,
|
|
80
|
+
M as useGraph
|
|
81
81
|
};
|
|
82
82
|
//# sourceMappingURL=use-graph.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-graph.js","sources":["../../src/react/use-graph.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createGraph, type GraphOptions, type GraphState, type GraphStore } from \"../core/store\";\nimport type { GraphCommands, SelectionSource, Tool, VertexId } from \"../core/types\";\nimport { createRenderer, type Renderer, type RendererEvents } from \"../render/renderer\";\n\nexport type UseGraphProps = GraphOptions;\n\n/**\n * **The commands, and the door to the state — stable for the life of the root.** Zag's split of an\n * api from its state, in TanStack Store's shape: the state is read through `useGraphState(selector)`\n * so a part re-renders on what it shows, and a host holding the api never re-renders because a\n * vertex was hovered.\n */\nexport interface GraphApi extends GraphCommands {\n select(vertices: readonly VertexId[] | null, source?: SelectionSource, label?: string): void;\n setFocus(vertex: VertexId | null): void;\n setTool(tool: Tool): void;\n subscribe(listener: () => void): () => void;\n /** The state now, for a callback; a render reads it with `useGraphState`. */\n getState(): GraphState;\n}\n\n/** What the parts in this package reach and a host does not: the element and the renderer. */\ninterface Internals {\n store: GraphStore;\n attach(host: HTMLDivElement, events?: RendererEvents): () => void;\n renderer(): Renderer | null;\n}\n\nconst INTERNALS = new WeakMap<GraphApi, Internals>();\n\n/** The parts' door to the renderer. Not on the barrel, and not on `GraphApi`'s type. */\nexport function internalsOf(api: GraphApi): Internals {\n const found = INTERNALS.get(api);\n if (!found) throw new Error(\"a graph part was given an api useGraph did not build\");\n return found;\n}\n\n/**\n * **`useBaseQuery`'s three moves**: the store is created once in `useState`, handed the latest props\n * with `setOptions` in an effect — whose dependencies are the props themselves, so a render that\n * changed none does not reach it — and read through `useSyncExternalStore`, in `useGraphState`.\n * Callbacks are read through a ref, so a host's inline `onFailure` is never a change of options.\n */\nexport function useGraph(props: UseGraphProps): GraphApi {\n const latest = useRef(props);\n latest.current = props;\n const forward = useCallback(\n (options: UseGraphProps): UseGraphProps => ({\n ...options,\n onFailure: (error) => latest.current.onFailure(error),\n onSelect: (selection) => latest.current.onSelect?.(selection),\n onFocus: (vertex) => latest.current.onFocus?.(vertex),\n }),\n [],\n );\n const [api] = useState<GraphApi>(() => build(createGraph(forward(props))));\n const { store } = internalsOf(api);\n\n const { categories,
|
|
1
|
+
{"version":3,"file":"use-graph.js","sources":["../../src/react/use-graph.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createGraph, type GraphOptions, type GraphState, type GraphStore } from \"../core/store\";\nimport type { GraphCommands, SelectionSource, Tool, VertexId } from \"../core/types\";\nimport { createRenderer, type Renderer, type RendererEvents } from \"../render/renderer\";\n\nexport type UseGraphProps = GraphOptions;\n\n/**\n * **The commands, and the door to the state — stable for the life of the root.** Zag's split of an\n * api from its state, in TanStack Store's shape: the state is read through `useGraphState(selector)`\n * so a part re-renders on what it shows, and a host holding the api never re-renders because a\n * vertex was hovered.\n */\nexport interface GraphApi extends GraphCommands {\n select(vertices: readonly VertexId[] | null, source?: SelectionSource, label?: string): void;\n setFocus(vertex: VertexId | null): void;\n setTool(tool: Tool): void;\n subscribe(listener: () => void): () => void;\n /** The state now, for a callback; a render reads it with `useGraphState`. */\n getState(): GraphState;\n}\n\n/** What the parts in this package reach and a host does not: the element and the renderer. */\ninterface Internals {\n store: GraphStore;\n attach(host: HTMLDivElement, events?: RendererEvents): () => void;\n renderer(): Renderer | null;\n}\n\nconst INTERNALS = new WeakMap<GraphApi, Internals>();\n\n/** The parts' door to the renderer. Not on the barrel, and not on `GraphApi`'s type. */\nexport function internalsOf(api: GraphApi): Internals {\n const found = INTERNALS.get(api);\n if (!found) throw new Error(\"a graph part was given an api useGraph did not build\");\n return found;\n}\n\n/**\n * **`useBaseQuery`'s three moves**: the store is created once in `useState`, handed the latest props\n * with `setOptions` in an effect — whose dependencies are the props themselves, so a render that\n * changed none does not reach it — and read through `useSyncExternalStore`, in `useGraphState`.\n * Callbacks are read through a ref, so a host's inline `onFailure` is never a change of options.\n */\nexport function useGraph(props: UseGraphProps): GraphApi {\n const latest = useRef(props);\n latest.current = props;\n const forward = useCallback(\n (options: UseGraphProps): UseGraphProps => ({\n ...options,\n onFailure: (error) => latest.current.onFailure(error),\n onSelect: (selection) => latest.current.onSelect?.(selection),\n onFocus: (vertex) => latest.current.onFocus?.(vertex),\n }),\n [],\n );\n const [api] = useState<GraphApi>(() => build(createGraph(forward(props))));\n const { store } = internalsOf(api);\n\n const { categories, cluster, coordinator, fill, filterBy, from, look, r, sim, simulate, stroke, symbol, title, x, y } = props;\n useEffect(() => {\n store.setOptions(forward(latest.current));\n }, [store, forward, categories, cluster, coordinator, fill, filterBy, from, look, r, sim, simulate, stroke, symbol, title, x, y]);\n\n // Subscribed here as well as by the parts, so the store's first-subscriber and last-subscriber\n // moves follow the root's lifetime and not whichever part happened to mount first.\n useEffect(() => store.subscribe(() => {}), [store]);\n\n return api;\n}\n\nfunction build(store: GraphStore): GraphApi {\n let renderer: Renderer | null = null;\n const on =\n <K extends keyof GraphCommands>(name: K) =>\n (...args: Parameters<GraphCommands[K]>) =>\n (renderer?.[name] as ((...a: Parameters<GraphCommands[K]>) => void) | undefined)?.(...args);\n const api: GraphApi = {\n zoomBy: on(\"zoomBy\"),\n fit: on(\"fit\"),\n pause: on(\"pause\"),\n resume: on(\"resume\"),\n restart: on(\"restart\"),\n unpin: on(\"unpin\"),\n // Selecting and focusing are state, and hold without a renderer; centring is the camera's.\n reveal: (vertex) => {\n if (renderer) return renderer.reveal(vertex);\n store.select([vertex], \"node\", \"Node\");\n store.focus(vertex);\n },\n frameSelection: on(\"frameSelection\"),\n clear: () => {\n store.select(null);\n store.focus(null);\n },\n select: (vertices, source, label) => store.select(vertices, source, label),\n setFocus: (vertex) => store.focus(vertex),\n setTool: (tool) => store.setTool(tool),\n subscribe: (listener) => store.subscribe(listener),\n getState: () => store.getSnapshot(),\n };\n INTERNALS.set(api, {\n store,\n renderer: () => renderer,\n attach(host, events) {\n store.renderable();\n let mounted: Renderer | null = null;\n try {\n mounted = createRenderer(host, store, events);\n } catch (error) {\n store.unrenderable(error);\n }\n renderer = mounted;\n return () => {\n mounted?.destroy();\n if (renderer === mounted) renderer = null;\n };\n },\n });\n return api;\n}\n\n"],"names":[],"mappings":";;;;AA+BA;AAGO;AACL;AACA;AACA;AACF;AAQO;AACL;AACA;AACA;AAAgB;AAC8B;AACvC;AACiD;;AAC3B;AAA0B;AAAA;;AAC9B;AAAyB;AAAA;AAAM;AAEtD;AAMF;AACE;AAAwC;AAKJ;AAGxC;AAEA;AACE;AACA;;AAGK;AAAqF;AACpE;AACD;AACN;AACI;AACE;AACE;AACJ;AAAA;AAGf;AACA;AACkB;AACpB;AACmC;AAEjC;AACgB;AAClB;AACyE;AACjC;AACH;AACY;AAC3B;AAExB;AAAmB;AACjB;AACgB;AAEd;AACA;AACA;AACE;AAA4C;AAE5C;AAAwB;AAE1B;AAEE;AACqC;AACvC;AACF;AAGJ;;;;;"}
|
package/dist/render/camera.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Graph } from '@cosmos.gl/graph';
|
|
2
|
+
import { GraphError } from '../core/error';
|
|
2
3
|
export declare const FIT_DURATION = 420;
|
|
3
4
|
export interface Camera {
|
|
4
5
|
/** A corpus was placed: frame these corners now, and again whenever the canvas changes size. */
|
|
@@ -7,6 +8,10 @@ export interface Camera {
|
|
|
7
8
|
fit(): void;
|
|
8
9
|
/** A layout started moving the points: the corners are history, and the camera follows. */
|
|
9
10
|
run(): void;
|
|
11
|
+
/** cosmos.gl's device came up: the frame is taken again, at the canvas's size. */
|
|
12
|
+
ready(): void;
|
|
13
|
+
/** cosmos.gl was handed positions and rendered them: from now on a fit may read them back. */
|
|
14
|
+
drawn(): void;
|
|
10
15
|
tick(): void;
|
|
11
16
|
settle(): void;
|
|
12
17
|
destroy(): void;
|
|
@@ -18,8 +23,14 @@ export interface Camera {
|
|
|
18
23
|
* zoomed into a corner — and while a layout runs it follows the moving points and frames them when
|
|
19
24
|
* they settle: Cosmograph's fit-on-settle, kept up. All of it stops the moment the reader zooms or
|
|
20
25
|
* pans, and comes back with Fit or the next run.
|
|
26
|
+
*
|
|
27
|
+
* **A fit without corners reads the points back from the GPU** (`fitView` → `getPointPositions` →
|
|
28
|
+
* `readPixels` of cosmos.gl's position framebuffer), so it waits until there is one to read: the
|
|
29
|
+
* device is up and a render has uploaded positions. If the read throws anyway, cosmos.gl holds no
|
|
30
|
+
* positions: that is `graph/no-positions`, reported once through `fail`, and the camera reads no more
|
|
31
|
+
* — rather than the same exception on every tick of a layout over a blank canvas.
|
|
21
32
|
*/
|
|
22
|
-
export declare function createCamera(graph: Graph, host: HTMLElement): {
|
|
33
|
+
export declare function createCamera(graph: Graph, host: HTMLElement, fail: (error: GraphError) => void): {
|
|
23
34
|
camera: Camera;
|
|
24
35
|
taken: (userDriven: boolean) => void;
|
|
25
36
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../../src/render/camera.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../../src/render/camera.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,eAAO,MAAM,YAAY,MAAM,CAAC;AAMhC,MAAM,WAAW,MAAM;IACrB,gGAAgG;IAChG,KAAK,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACxC,2FAA2F;IAC3F,GAAG,IAAI,IAAI,CAAC;IACZ,2FAA2F;IAC3F,GAAG,IAAI,IAAI,CAAC;IACZ,kFAAkF;IAClF,KAAK,IAAI,IAAI,CAAC;IACd,8FAA8F;IAC9F,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;IACb,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAChC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,CA6E1D"}
|
package/dist/render/camera.js
CHANGED
|
@@ -1,40 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
1
|
+
import { GraphError as A } from "../core/error.js";
|
|
2
|
+
const d = 420, p = 0.18, I = 900, P = 700;
|
|
3
|
+
function D(a, O, y) {
|
|
4
|
+
let r = null, s = !1, t = !1, f = 0, o = 0, c = !1, u = !1, m = !1;
|
|
5
|
+
const i = (e) => {
|
|
6
|
+
if (!(!c || u || !a.isReady))
|
|
7
|
+
try {
|
|
8
|
+
a.fitView(e, p);
|
|
9
|
+
} catch (R) {
|
|
10
|
+
u = !0, t = !1, y(new A("graph/no-positions", "cosmos.gl holds no positions on the GPU to draw or frame.", {}, { cause: R }));
|
|
11
|
+
}
|
|
12
|
+
}, l = (e) => {
|
|
13
|
+
r ? a.fitViewByPointPositions([...r], e, p) : i(e);
|
|
14
|
+
}, F = () => {
|
|
15
|
+
o = 0, s && !t && l(0);
|
|
16
|
+
}, w = () => {
|
|
17
|
+
!o && !m && (o = requestAnimationFrame(F));
|
|
18
|
+
}, n = typeof ResizeObserver > "u" ? null : new ResizeObserver(w);
|
|
19
|
+
return n == null || n.observe(O), { camera: {
|
|
13
20
|
frame(e) {
|
|
14
|
-
|
|
21
|
+
r = e, s = !0, l(0);
|
|
15
22
|
},
|
|
16
23
|
fit() {
|
|
17
|
-
|
|
24
|
+
s = !0, l(d);
|
|
18
25
|
},
|
|
19
26
|
run() {
|
|
20
|
-
|
|
27
|
+
r = null, t = !0, f = performance.now();
|
|
28
|
+
},
|
|
29
|
+
// A frame later: the commands cosmos.gl queued for its device run in the same turn as this.
|
|
30
|
+
ready: w,
|
|
31
|
+
drawn() {
|
|
32
|
+
c = !0;
|
|
21
33
|
},
|
|
22
34
|
tick() {
|
|
23
35
|
const e = performance.now();
|
|
24
|
-
!
|
|
36
|
+
!t || e - f < I || (f = e, i(P));
|
|
25
37
|
},
|
|
26
38
|
settle() {
|
|
27
|
-
|
|
39
|
+
t && i(d), t = !1;
|
|
28
40
|
},
|
|
29
41
|
destroy() {
|
|
30
|
-
|
|
42
|
+
m = !0, n == null || n.disconnect(), o && cancelAnimationFrame(o);
|
|
31
43
|
}
|
|
32
44
|
}, taken: (e) => {
|
|
33
|
-
e && (
|
|
45
|
+
e && (s = !1, t = !1);
|
|
34
46
|
} };
|
|
35
47
|
}
|
|
36
48
|
export {
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
d as FIT_DURATION,
|
|
50
|
+
D as createCamera
|
|
39
51
|
};
|
|
40
52
|
//# sourceMappingURL=camera.js.map
|