@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.
Files changed (85) hide show
  1. package/README.md +2 -2
  2. package/dist/core/categories.d.ts +1 -1
  3. package/dist/core/categories.d.ts.map +1 -1
  4. package/dist/core/categories.js.map +1 -1
  5. package/dist/core/channels.d.ts +12 -6
  6. package/dist/core/channels.d.ts.map +1 -1
  7. package/dist/core/channels.js +15 -12
  8. package/dist/core/channels.js.map +1 -1
  9. package/dist/core/client.d.ts +26 -0
  10. package/dist/core/client.d.ts.map +1 -0
  11. package/dist/core/client.js +62 -0
  12. package/dist/core/client.js.map +1 -0
  13. package/dist/core/error.d.ts +3 -3
  14. package/dist/core/error.d.ts.map +1 -1
  15. package/dist/core/error.js.map +1 -1
  16. package/dist/core/load.d.ts +30 -39
  17. package/dist/core/load.d.ts.map +1 -1
  18. package/dist/core/load.js +63 -117
  19. package/dist/core/load.js.map +1 -1
  20. package/dist/core/source.d.ts +90 -0
  21. package/dist/core/source.d.ts.map +1 -0
  22. package/dist/core/source.js +105 -0
  23. package/dist/core/source.js.map +1 -0
  24. package/dist/core/state.d.ts +19 -20
  25. package/dist/core/state.d.ts.map +1 -1
  26. package/dist/core/store.d.ts.map +1 -1
  27. package/dist/core/store.js +138 -158
  28. package/dist/core/store.js.map +1 -1
  29. package/dist/core/types.d.ts +5 -3
  30. package/dist/core/types.d.ts.map +1 -1
  31. package/dist/index.d.ts +3 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +20 -18
  34. package/dist/index.js.map +1 -1
  35. package/dist/parts/graph-canvas.d.ts.map +1 -1
  36. package/dist/parts/graph-canvas.js +49 -48
  37. package/dist/parts/graph-canvas.js.map +1 -1
  38. package/dist/parts/graph-counts.js +4 -4
  39. package/dist/parts/graph-counts.js.map +1 -1
  40. package/dist/parts/graph-inspector.d.ts +3 -3
  41. package/dist/parts/graph-inspector.d.ts.map +1 -1
  42. package/dist/parts/graph-inspector.js +45 -43
  43. package/dist/parts/graph-inspector.js.map +1 -1
  44. package/dist/parts/graph-search.d.ts +7 -7
  45. package/dist/parts/graph-search.d.ts.map +1 -1
  46. package/dist/parts/graph-search.js +60 -69
  47. package/dist/parts/graph-search.js.map +1 -1
  48. package/dist/parts/graph-select.d.ts +27 -0
  49. package/dist/parts/graph-select.d.ts.map +1 -0
  50. package/dist/parts/graph-select.js +53 -0
  51. package/dist/parts/graph-select.js.map +1 -0
  52. package/dist/parts/graph-toolbar.js +1 -1
  53. package/dist/parts/graph-toolbar.js.map +1 -1
  54. package/dist/react/use-graph.js +31 -31
  55. package/dist/react/use-graph.js.map +1 -1
  56. package/dist/render/camera.d.ts +12 -1
  57. package/dist/render/camera.d.ts.map +1 -1
  58. package/dist/render/camera.js +33 -21
  59. package/dist/render/camera.js.map +1 -1
  60. package/dist/render/graph-sim.d.ts +1 -1
  61. package/dist/render/graph-sim.d.ts.map +1 -1
  62. package/dist/render/graph-sim.js.map +1 -1
  63. package/dist/render/motion.d.ts +33 -0
  64. package/dist/render/motion.d.ts.map +1 -0
  65. package/dist/render/motion.js +19 -0
  66. package/dist/render/motion.js.map +1 -0
  67. package/dist/render/placement.d.ts +2 -4
  68. package/dist/render/placement.d.ts.map +1 -1
  69. package/dist/render/placement.js +12 -16
  70. package/dist/render/placement.js.map +1 -1
  71. package/dist/render/renderer.d.ts +6 -5
  72. package/dist/render/renderer.d.ts.map +1 -1
  73. package/dist/render/renderer.js +130 -118
  74. package/dist/render/renderer.js.map +1 -1
  75. package/dist/section.js +1 -1
  76. package/dist/section.js.map +1 -1
  77. package/package.json +12 -14
  78. package/dist/core/detail.d.ts +0 -26
  79. package/dist/core/detail.d.ts.map +0 -1
  80. package/dist/core/detail.js +0 -63
  81. package/dist/core/detail.js.map +0 -1
  82. package/dist/core/filter.d.ts +0 -23
  83. package/dist/core/filter.d.ts.map +0 -1
  84. package/dist/core/filter.js +0 -95
  85. package/dist/core/filter.js.map +0 -1
@@ -1,81 +1,72 @@
1
1
  "use client";
2
- import { jsxs as h, jsx as r } from "react/jsx-runtime";
3
- import { useFilter as z, createListCollection as A, Combobox as O, ComboboxInput as q, InputGroupAddon as K, ComboboxContent as L, ComboboxEmpty as M, ComboboxItem as Q, Show as E } from "@kanzo-tech/ui";
4
- import { SearchIcon as R } from "lucide-react";
5
- import { useState as T, useEffect as $, useMemo as V } from "react";
6
- import { nameOf as B } from "../core/categories.js";
7
- import { bindingOf as D } from "../core/channels.js";
8
- import { readEveryTitle as H } from "../core/detail.js";
9
- import { useGraphContext as J } from "../react/graph-root.js";
10
- import { useGraphState as N, useGraphSnapshot as v } from "../react/use-graph-state.js";
11
- function P(m, s, d, f, a) {
12
- const i = [];
13
- for (let t = 0; t < m.size; t++)
14
- (!d || d[t]) && !Number.isNaN(m.positions[t * 2]) && i.push(t);
15
- const { sizes: l } = s;
16
- if (l) {
17
- const t = (o) => Number.isNaN(l[o]) ? -1 / 0 : l[o];
18
- i.sort((o, c) => t(c) - t(o) || o - c);
19
- }
20
- return i.map((t) => ({
21
- value: String(t),
22
- label: f[t] || `#${t}`,
23
- kind: a ? a(s.ranks[t]) : null
24
- }));
25
- }
26
- function oe({ className: m, limit: s = 50, placeholder: d = "Find a node…", size: f = "sm" }) {
27
- const a = J(), i = N((e) => e.corpus), l = N((e) => e.options), t = v((e) => e.geometry), o = v((e) => e.encoding), c = v((e) => e.mask), w = N((e) => {
28
- var n;
29
- return ((n = e.drawn) == null ? void 0 : n.domain) ?? e.domain;
30
- }), [p, S] = T(null), [b, j] = T(""), { contains: k } = z({ sensitivity: "base" }), { title: u, categories: I } = l;
31
- $(() => {
32
- if (!i || !t) return;
33
- const e = new AbortController();
34
- return H(i, t, u, e.signal).then(
35
- (n) => !e.signal.aborted && S({ geometry: t, title: u, titles: n }),
36
- (n) => {
37
- e.signal.aborted || (S({ geometry: t, title: u, titles: !1 }), a.getState().options.onFailure(n));
38
- }
39
- ), () => e.abort();
40
- }, [a, i, t, u]);
41
- const g = p && p.geometry === t && p.title === u ? p.titles : null, x = g || null, G = D(l), F = G.byTable || G.category !== void 0, y = V(() => !t || !o || !x ? null : P(t, o, c, x, F ? (e) => B(w[e], I) : null), [t, o, c, x, F, w, I]), C = V(() => {
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: C,
53
- disabled: y === null,
54
- onInputValueChange: (e) => j(e.inputValue),
43
+ collection: m,
44
+ disabled: a === null,
45
+ onInputValueChange: (e) => T(e.inputValue),
55
46
  onValueChange: (e) => {
56
- const n = e.value[0];
57
- n !== void 0 && a.reveal(Number(n));
47
+ const o = e.value[0];
48
+ o !== void 0 && l.reveal(Number(o));
58
49
  },
59
50
  children: [
60
- /* @__PURE__ */ r(
61
- q,
51
+ /* @__PURE__ */ n(
52
+ j,
62
53
  {
63
- "aria-invalid": g === !1 || void 0,
64
- className: m,
65
- placeholder: g === !1 ? "The names could not be read." : d,
66
- size: f,
67
- children: /* @__PURE__ */ r(K, { align: "inline-start", children: /* @__PURE__ */ r(R, {}) })
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__ */ h(L, { children: [
71
- /* @__PURE__ */ r(M, { children: "Nothing drawn by that name." }),
72
- C.items.map((e) => /* @__PURE__ */ h(Q, { item: e, children: [
73
- /* @__PURE__ */ r("span", { className: "min-w-0 truncate", children: e.label }),
74
- /* @__PURE__ */ r(E, { when: e.kind !== null, children: /* @__PURE__ */ r("span", { className: "ms-auto ps-2 text-muted-foreground text-xs", children: e.kind }) })
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__ */ r(E, { when: C.items.length === s, children: /* @__PURE__ */ h("p", { className: "border-t px-2 py-1.5 text-muted-foreground text-xs", children: [
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
- s,
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
- oe as GraphSearch
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 useFilter,\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 { readEveryTitle } from \"../core/detail\";\nimport type { Encoding, Geometry } from \"../core/load\";\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/** The titles read for one geometry and title column, or `false` when the read failed. */\ntype Titles = { geometry: Geometry; title: string | undefined; titles: string[] | false } | null;\n\n/** Every drawn vertex, biggest on the `r` ramp first — the vertices a reader most likely means. */\nfunction entriesOf(\n geometry: Geometry,\n encoding: Encoding,\n mask: Uint8Array | null,\n titles: readonly string[],\n kindOf: ((rank: number) => string) | null,\n): Entry[] {\n const ids: number[] = [];\n for (let id = 0; id < geometry.size; id++) {\n if ((!mask || mask[id]) && !Number.isNaN(geometry.positions[id * 2])) ids.push(id);\n }\n const { sizes } = encoding;\n if (sizes) {\n const ramp = (id: number) => (Number.isNaN(sizes[id]) ? -Infinity : (sizes[id] as number));\n ids.sort((a, b) => ramp(b) - ramp(a) || a - b);\n }\n return ids.map((id) => ({\n value: String(id),\n label: titles[id] || `#${id}`,\n kind: kindOf ? kindOf(encoding.ranks[id] as number) : null,\n }));\n}\n\n/**\n * **Find a vertex by its text and go to it.** Every drawn vertex's `title` — or its table's\n * `identity` — is read once, then filtered in the browser as the reader types: no query per\n * keystroke. The list is ordered by the `r` ramp, so with `r=\"degree\"` the hubs come first, and each\n * match is named by the root's `categories`. Picking one is `reveal`: the canvas frames it and\n * selects it with its neighbours, and `GraphInspector` reads it.\n *\n * A vertex the page's filter hides is not offered, because there is nothing on the canvas to go to.\n * A read that fails is handed to `onFailure` whole and the input says so, rather than staying disabled.\n */\nexport function GraphSearch({ className, limit = 50, placeholder = \"Find a node…\", size = \"sm\" }: GraphSearchProps) {\n const api = useGraphContext();\n const corpus = useGraphState((s) => s.corpus);\n const options = useGraphState((s) => s.options);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const encoding = useGraphSnapshot((s) => s.encoding);\n const mask = useGraphSnapshot((s) => s.mask);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const [read, setRead] = useState<Titles>(null);\n const [query, setQuery] = useState(\"\");\n const { contains } = useFilter({ sensitivity: \"base\" });\n const { title, categories } = options;\n\n useEffect(() => {\n if (!corpus || !geometry) return;\n const aborter = new AbortController();\n readEveryTitle(corpus, geometry, title, aborter.signal).then(\n (titles) => !aborter.signal.aborted && setRead({ geometry, title, titles }),\n (error: unknown) => {\n if (aborter.signal.aborted) return;\n setRead({ geometry, title, titles: false });\n api.getState().options.onFailure(error);\n },\n );\n return () => aborter.abort();\n }, [api, corpus, geometry, title]);\n\n const answered = read && read.geometry === geometry && read.title === title ? read.titles : null;\n const titles = answered || null;\n const binding = bindingOf(options);\n const bound = binding.byTable || binding.category !== undefined;\n\n const entries = useMemo(() => {\n if (!geometry || !encoding || !titles) return null;\n return entriesOf(geometry, encoding, mask, titles, bound ? (rank) => nameOf(domain[rank], categories) : null);\n }, [geometry, encoding, mask, titles, bound, domain, categories]);\n\n const collection = useMemo(() => {\n const matches: Entry[] = [];\n for (const entry of entries ?? []) {\n if (matches.length === limit) break;\n if (!query || contains(entry.label, query)) matches.push(entry);\n }\n return createListCollection({ items: matches });\n }, [entries, query, contains, limit]);\n\n return (\n <Combobox\n collection={collection}\n disabled={entries === 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 drawn 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":";;;;;;;;;;AAqCA;AAOE;AACA;AACE;AAEF;AACA;AACE;AACA;AAA6C;AAE/C;AAAwB;AACN;AACW;AAC2B;AAE1D;AAYO;AACL;;AAMoC;AAAqB;AAMzD;AACE;AACA;AACA;AAAwD;AACoB;AAExE;AAEsC;AACxC;AAEmB;AAGvB;AAWE;AACA;AACE;AACA;AAA8D;AAEhE;AAA8C;AAGhD;AACE;AAAC;AAAA;AACC;AACsB;AACsC;AAE1D;AACA;AAAmD;AACrD;AAEA;AAAA;AAAC;AAAA;AACqC;AACpC;AACmE;AACnE;AAIA;AAAA;AAAA;AAGA;AAA0C;AAGtC;AAA+C;AAG/C;AAEH;AAEmE;AAAA;AAAO;AAAM;AACjF;AACF;AAAA;AAAA;AAGN;;;;"}
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.corpus !== 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"}`;
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.corpus !== 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;;;;"}
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;;;;"}
@@ -1,19 +1,19 @@
1
1
  "use client";
2
- import { useRef as k, useCallback as B, useState as E, useEffect as f } from "react";
3
- import { createGraph as G } from "../core/store.js";
4
- import { createRenderer as N } from "../render/renderer.js";
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 R(e) {
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 A(e) {
12
- const t = k(e);
11
+ function M(e) {
12
+ const t = N(e);
13
13
  t.current = e;
14
- const u = B(
15
- (F) => ({
16
- ...F,
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
- ), [o] = E(() => T(G(u(e)))), { store: n } = R(o), { categories: c, corpus: r, fill: i, filterBy: p, look: b, r: d, sim: h, simulate: S, stroke: g, symbol: y, title: w } = e;
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(u(t.current));
31
- }, [n, u, c, r, i, p, b, d, h, S, g, y, w]), f(() => n.subscribe(() => {
32
- }), [n]), o;
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 T(e) {
34
+ function A(e) {
35
35
  let t = null;
36
- const u = (n) => (...c) => {
36
+ const o = (n) => (...u) => {
37
37
  var r;
38
- return (r = t == null ? void 0 : t[n]) == null ? void 0 : r.call(t, ...c);
39
- }, o = {
40
- zoomBy: u("zoomBy"),
41
- fit: u("fit"),
42
- pause: u("pause"),
43
- resume: u("resume"),
44
- restart: u("restart"),
45
- unpin: u("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: u("frameSelection"),
51
+ frameSelection: o("frameSelection"),
52
52
  clear: () => {
53
53
  e.select(null), e.focus(null);
54
54
  },
55
- select: (n, c, r) => e.select(n, c, r),
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(o, {
61
+ return m.set(c, {
62
62
  store: e,
63
63
  renderer: () => t,
64
- attach(n, c) {
64
+ attach(n, u) {
65
65
  e.renderable();
66
66
  let r = null;
67
67
  try {
68
- r = N(n, e, c);
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
- }), o;
76
+ }), c;
77
77
  }
78
78
  export {
79
- R as internalsOf,
80
- A as useGraph
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, corpus, fill, filterBy, look, r, sim, simulate, stroke, symbol, title } = props;\n useEffect(() => {\n store.setOptions(forward(latest.current));\n }, [store, forward, categories, corpus, fill, filterBy, look, r, sim, simulate, stroke, symbol, title]);\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;;;;;"}
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;;;;;"}
@@ -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;AAE9C,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,IAAI,IAAI,IAAI,CAAC;IACb,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,CA6DtH"}
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"}
@@ -1,40 +1,52 @@
1
- const O = 420;
2
- function F(o, a) {
3
- let f = null, r = !1, n = !1, l = 0, i = 0;
4
- const c = (e) => {
5
- f ? o.fitViewByPointPositions([...f], e, 0.18) : o.fitView(e, 0.18);
6
- }, s = () => {
7
- i = 0, r && !n && c(0);
8
- }, t = typeof ResizeObserver > "u" ? null : new ResizeObserver(() => {
9
- i || (i = requestAnimationFrame(s));
10
- });
11
- return t == null || t.observe(a), o.ready.then(s, () => {
12
- }), { camera: {
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
- f = e, r = !0, c(0);
21
+ r = e, s = !0, l(0);
15
22
  },
16
23
  fit() {
17
- r = !0, c(420);
24
+ s = !0, l(d);
18
25
  },
19
26
  run() {
20
- f = null, n = !0, l = performance.now();
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
- !n || e - l < 900 || (l = e, o.fitView(700, 0.18));
36
+ !t || e - f < I || (f = e, i(P));
25
37
  },
26
38
  settle() {
27
- n && o.fitView(420, 0.18), n = !1;
39
+ t && i(d), t = !1;
28
40
  },
29
41
  destroy() {
30
- t == null || t.disconnect(), i && cancelAnimationFrame(i);
42
+ m = !0, n == null || n.disconnect(), o && cancelAnimationFrame(o);
31
43
  }
32
44
  }, taken: (e) => {
33
- e && (r = !1, n = !1);
45
+ e && (s = !1, t = !1);
34
46
  } };
35
47
  }
36
48
  export {
37
- O as FIT_DURATION,
38
- F as createCamera
49
+ d as FIT_DURATION,
50
+ D as createCamera
39
51
  };
40
52
  //# sourceMappingURL=camera.js.map