@kanzo-tech/graph 0.10.0 → 0.12.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 (204) hide show
  1. package/README.md +60 -95
  2. package/dist/core/categories.d.ts +12 -0
  3. package/dist/core/categories.d.ts.map +1 -0
  4. package/dist/core/categories.js +14 -0
  5. package/dist/core/categories.js.map +1 -0
  6. package/dist/core/channels.d.ts +34 -0
  7. package/dist/core/channels.d.ts.map +1 -0
  8. package/dist/core/channels.js +19 -0
  9. package/dist/core/channels.js.map +1 -0
  10. package/dist/core/detail.d.ts +18 -0
  11. package/dist/core/detail.d.ts.map +1 -0
  12. package/dist/core/detail.js +20 -0
  13. package/dist/core/detail.js.map +1 -0
  14. package/dist/core/filter.d.ts +34 -0
  15. package/dist/core/filter.d.ts.map +1 -0
  16. package/dist/core/filter.js +99 -0
  17. package/dist/core/filter.js.map +1 -0
  18. package/dist/core/refine.d.ts +14 -0
  19. package/dist/core/refine.d.ts.map +1 -0
  20. package/dist/core/refine.js +20 -0
  21. package/dist/core/refine.js.map +1 -0
  22. package/dist/{resident.d.ts → core/resident.d.ts} +7 -7
  23. package/dist/core/resident.d.ts.map +1 -0
  24. package/dist/core/resident.js +44 -0
  25. package/dist/core/resident.js.map +1 -0
  26. package/dist/core/scheduler.d.ts +42 -0
  27. package/dist/core/scheduler.d.ts.map +1 -0
  28. package/dist/core/scheduler.js +77 -0
  29. package/dist/core/scheduler.js.map +1 -0
  30. package/dist/core/state.d.ts +132 -0
  31. package/dist/core/state.d.ts.map +1 -0
  32. package/dist/core/store.d.ts +6 -0
  33. package/dist/core/store.d.ts.map +1 -0
  34. package/dist/core/store.js +235 -0
  35. package/dist/core/store.js.map +1 -0
  36. package/dist/core/tile-matrix.d.ts +37 -0
  37. package/dist/core/tile-matrix.d.ts.map +1 -0
  38. package/dist/core/tile-matrix.js +49 -0
  39. package/dist/core/tile-matrix.js.map +1 -0
  40. package/dist/core/tile.d.ts +54 -0
  41. package/dist/core/tile.d.ts.map +1 -0
  42. package/dist/core/tile.js +76 -0
  43. package/dist/core/tile.js.map +1 -0
  44. package/dist/core/tileset.d.ts +59 -0
  45. package/dist/core/tileset.d.ts.map +1 -0
  46. package/dist/core/tileset.js +177 -0
  47. package/dist/core/tileset.js.map +1 -0
  48. package/dist/{types.d.ts → core/types.d.ts} +9 -0
  49. package/dist/core/types.d.ts.map +1 -0
  50. package/dist/index.d.ts +37 -70
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +32 -34
  53. package/dist/index.js.map +1 -1
  54. package/dist/{use-graph-selection.d.ts → parts/gesture.d.ts} +4 -4
  55. package/dist/parts/gesture.d.ts.map +1 -0
  56. package/dist/{use-graph-selection.js → parts/gesture.js} +17 -17
  57. package/dist/parts/gesture.js.map +1 -0
  58. package/dist/parts/graph-canvas.d.ts +17 -0
  59. package/dist/parts/graph-canvas.d.ts.map +1 -0
  60. package/dist/parts/graph-canvas.js +174 -0
  61. package/dist/parts/graph-canvas.js.map +1 -0
  62. package/dist/parts/graph-inspector.d.ts +18 -0
  63. package/dist/parts/graph-inspector.d.ts.map +1 -0
  64. package/dist/parts/graph-inspector.js +84 -0
  65. package/dist/parts/graph-inspector.js.map +1 -0
  66. package/dist/parts/graph-legend.d.ts +12 -0
  67. package/dist/parts/graph-legend.d.ts.map +1 -0
  68. package/dist/parts/graph-legend.js +61 -0
  69. package/dist/parts/graph-legend.js.map +1 -0
  70. package/dist/parts/graph-toolbar.d.ts +15 -0
  71. package/dist/parts/graph-toolbar.d.ts.map +1 -0
  72. package/dist/parts/graph-toolbar.js +94 -0
  73. package/dist/parts/graph-toolbar.js.map +1 -0
  74. package/dist/parts/overlays.d.ts +33 -0
  75. package/dist/parts/overlays.d.ts.map +1 -0
  76. package/dist/{use-graph-overlays.js → parts/overlays.js} +8 -8
  77. package/dist/parts/overlays.js.map +1 -0
  78. package/dist/{shape-glyph.d.ts → parts/shape-glyph.d.ts} +1 -1
  79. package/dist/parts/shape-glyph.d.ts.map +1 -0
  80. package/dist/{shape-glyph.js → parts/shape-glyph.js} +1 -1
  81. package/dist/parts/shape-glyph.js.map +1 -0
  82. package/dist/react/graph-root.d.ts +15 -0
  83. package/dist/react/graph-root.d.ts.map +1 -0
  84. package/dist/react/graph-root.js +23 -0
  85. package/dist/react/graph-root.js.map +1 -0
  86. package/dist/{use-graph-prefs.d.ts → react/use-graph-prefs.d.ts} +2 -2
  87. package/dist/react/use-graph-prefs.d.ts.map +1 -0
  88. package/dist/{use-graph-prefs.js → react/use-graph-prefs.js} +3 -3
  89. package/dist/react/use-graph-prefs.js.map +1 -0
  90. package/dist/react/use-graph-state.d.ts +11 -0
  91. package/dist/react/use-graph-state.d.ts.map +1 -0
  92. package/dist/react/use-graph-state.js +16 -0
  93. package/dist/react/use-graph-state.js.map +1 -0
  94. package/dist/react/use-graph.d.ts +38 -0
  95. package/dist/react/use-graph.d.ts.map +1 -0
  96. package/dist/react/use-graph.js +87 -0
  97. package/dist/react/use-graph.js.map +1 -0
  98. package/dist/{adaptive.d.ts → render/adaptive.d.ts} +1 -1
  99. package/dist/render/adaptive.d.ts.map +1 -0
  100. package/dist/render/adaptive.js.map +1 -0
  101. package/dist/render/compose.d.ts +51 -0
  102. package/dist/render/compose.d.ts.map +1 -0
  103. package/dist/render/compose.js +107 -0
  104. package/dist/render/compose.js.map +1 -0
  105. package/dist/render/css-color.d.ts.map +1 -0
  106. package/dist/render/css-color.js.map +1 -0
  107. package/dist/render/encode.d.ts +42 -0
  108. package/dist/render/encode.d.ts.map +1 -0
  109. package/dist/render/encode.js +68 -0
  110. package/dist/render/encode.js.map +1 -0
  111. package/dist/render/graph-looks.d.ts.map +1 -0
  112. package/dist/render/graph-looks.js.map +1 -0
  113. package/dist/render/graph-model.d.ts +44 -0
  114. package/dist/render/graph-model.d.ts.map +1 -0
  115. package/dist/render/graph-model.js +80 -0
  116. package/dist/render/graph-model.js.map +1 -0
  117. package/dist/{graph-sim.d.ts → render/graph-sim.d.ts} +1 -1
  118. package/dist/render/graph-sim.d.ts.map +1 -0
  119. package/dist/render/graph-sim.js.map +1 -0
  120. package/dist/{obligations.d.ts → render/obligations.d.ts} +1 -1
  121. package/dist/render/obligations.d.ts.map +1 -0
  122. package/dist/render/renderer.d.ts +38 -0
  123. package/dist/render/renderer.d.ts.map +1 -0
  124. package/dist/render/renderer.js +235 -0
  125. package/dist/render/renderer.js.map +1 -0
  126. package/dist/render/webgl.d.ts +14 -0
  127. package/dist/render/webgl.d.ts.map +1 -0
  128. package/dist/render/webgl.js +19 -0
  129. package/dist/render/webgl.js.map +1 -0
  130. package/dist/render/when-ready.d.ts.map +1 -0
  131. package/dist/render/when-ready.js.map +1 -0
  132. package/package.json +11 -21
  133. package/dist/adaptive.d.ts.map +0 -1
  134. package/dist/adaptive.js.map +0 -1
  135. package/dist/bounded.d.ts +0 -364
  136. package/dist/bounded.d.ts.map +0 -1
  137. package/dist/bounded.js +0 -18
  138. package/dist/bounded.js.map +0 -1
  139. package/dist/cluster-ring.d.ts +0 -25
  140. package/dist/cluster-ring.d.ts.map +0 -1
  141. package/dist/cluster-ring.js +0 -16
  142. package/dist/cluster-ring.js.map +0 -1
  143. package/dist/css-color.d.ts.map +0 -1
  144. package/dist/css-color.js.map +0 -1
  145. package/dist/duck-source.d.ts +0 -198
  146. package/dist/duck-source.d.ts.map +0 -1
  147. package/dist/duck-source.js +0 -320
  148. package/dist/duck-source.js.map +0 -1
  149. package/dist/graph-canvas.d.ts +0 -50
  150. package/dist/graph-canvas.d.ts.map +0 -1
  151. package/dist/graph-canvas.js +0 -35
  152. package/dist/graph-canvas.js.map +0 -1
  153. package/dist/graph-looks.d.ts.map +0 -1
  154. package/dist/graph-looks.js.map +0 -1
  155. package/dist/graph-model.d.ts +0 -130
  156. package/dist/graph-model.d.ts.map +0 -1
  157. package/dist/graph-model.js +0 -104
  158. package/dist/graph-model.js.map +0 -1
  159. package/dist/graph-sim.d.ts.map +0 -1
  160. package/dist/graph-sim.js.map +0 -1
  161. package/dist/obligations.d.ts.map +0 -1
  162. package/dist/resident.d.ts.map +0 -1
  163. package/dist/resident.js +0 -44
  164. package/dist/resident.js.map +0 -1
  165. package/dist/shape-glyph.d.ts.map +0 -1
  166. package/dist/shape-glyph.js.map +0 -1
  167. package/dist/slice-client.d.ts +0 -78
  168. package/dist/slice-client.d.ts.map +0 -1
  169. package/dist/slice-client.js +0 -98
  170. package/dist/slice-client.js.map +0 -1
  171. package/dist/types.d.ts.map +0 -1
  172. package/dist/use-graph-look.d.ts +0 -28
  173. package/dist/use-graph-look.d.ts.map +0 -1
  174. package/dist/use-graph-look.js +0 -28
  175. package/dist/use-graph-look.js.map +0 -1
  176. package/dist/use-graph-overlays.d.ts +0 -44
  177. package/dist/use-graph-overlays.d.ts.map +0 -1
  178. package/dist/use-graph-overlays.js.map +0 -1
  179. package/dist/use-graph-prefs.d.ts.map +0 -1
  180. package/dist/use-graph-prefs.js.map +0 -1
  181. package/dist/use-graph-selection.d.ts.map +0 -1
  182. package/dist/use-graph-selection.js.map +0 -1
  183. package/dist/use-graph.d.ts +0 -203
  184. package/dist/use-graph.d.ts.map +0 -1
  185. package/dist/use-graph.js +0 -102
  186. package/dist/use-graph.js.map +0 -1
  187. package/dist/use-query-loop.d.ts +0 -88
  188. package/dist/use-query-loop.d.ts.map +0 -1
  189. package/dist/use-query-loop.js +0 -148
  190. package/dist/use-query-loop.js.map +0 -1
  191. package/dist/use-renderer.d.ts +0 -86
  192. package/dist/use-renderer.d.ts.map +0 -1
  193. package/dist/use-renderer.js +0 -188
  194. package/dist/use-renderer.js.map +0 -1
  195. package/dist/when-ready.d.ts.map +0 -1
  196. package/dist/when-ready.js.map +0 -1
  197. /package/dist/{adaptive.js → render/adaptive.js} +0 -0
  198. /package/dist/{css-color.d.ts → render/css-color.d.ts} +0 -0
  199. /package/dist/{css-color.js → render/css-color.js} +0 -0
  200. /package/dist/{graph-looks.d.ts → render/graph-looks.d.ts} +0 -0
  201. /package/dist/{graph-looks.js → render/graph-looks.js} +0 -0
  202. /package/dist/{graph-sim.js → render/graph-sim.js} +0 -0
  203. /package/dist/{when-ready.d.ts → render/when-ready.d.ts} +0 -0
  204. /package/dist/{when-ready.js → render/when-ready.js} +0 -0
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { jsxs as s, jsx as e, Fragment as k } from "react/jsx-runtime";
3
+ import { useChartCapacity as I, Show as p, Skeleton as D, Badge as L, Button as z, DataList as G, DataListItem as B, DataListItemLabel as F, DataListItemValue as M, cn as j } from "@kanzo-tech/ui";
4
+ import { CrosshairIcon as A } from "lucide-react";
5
+ import { useMemo as E, useState as V, useEffect as T } from "react";
6
+ import { nameOf as $ } from "../core/categories.js";
7
+ import { bindingOf as q } from "../core/channels.js";
8
+ import { readVertex as H } from "../core/detail.js";
9
+ import { denseOf as J } from "../core/resident.js";
10
+ import { useGraphContext as K } from "../react/graph-root.js";
11
+ import { internalsOf as P } from "../react/use-graph.js";
12
+ import { useGraphState as d } from "../react/use-graph-state.js";
13
+ import { scaleOf as Q } from "../render/graph-model.js";
14
+ import { ShapeGlyph as R } from "./shape-glyph.js";
15
+ const w = (a) => a instanceof Date ? a.toISOString().slice(0, 10) : a == null || a === "" ? "—" : String(a);
16
+ function it({ children: a, className: S, slot: y, ...C }) {
17
+ const c = K(), o = d((t) => t.focus), h = d((t) => t.corpus), l = d((t) => t.options), O = d((t) => {
18
+ var r;
19
+ return ((r = t.drawn) == null ? void 0 : r.domain) ?? t.domain;
20
+ }), u = I(), x = E(() => Q(l, u), [l, u]), [f, g] = V(null);
21
+ T(() => {
22
+ const t = P(c).store.getSnapshot().matrix;
23
+ if (o === null || !h || !t) return;
24
+ const r = new AbortController();
25
+ return H(h, t, o, r.signal).then(
26
+ (i) => !r.signal.aborted && g({ vertex: o, detail: i }),
27
+ (i) => {
28
+ r.signal.aborted || (g({ vertex: o, detail: null }), c.getState().options.onFailure(i instanceof Error ? i.message : String(i)));
29
+ }
30
+ ), () => r.abort();
31
+ }, [c, h, o]);
32
+ const n = f !== null && f.vertex === o ? f.detail : void 0, v = (t) => {
33
+ var r;
34
+ return (r = n == null ? void 0 : n.fields.find((i) => i.name === t)) == null ? void 0 : r.value;
35
+ }, N = q(l).category, m = v(N), b = O.indexOf(typeof m == "bigint" ? Number(m) : m);
36
+ return /* @__PURE__ */ s("div", { ...C, className: j("space-y-3 text-sm", S), "data-slot": y ?? "graph-inspector", children: [
37
+ /* @__PURE__ */ e(p, { when: o === null, children: /* @__PURE__ */ e("p", { className: "text-muted-foreground text-xs", children: "Click a vertex on the canvas to inspect it." }) }),
38
+ /* @__PURE__ */ e(p, { when: o !== null && n === void 0, children: /* @__PURE__ */ e(D, { className: "h-24 w-full" }) }),
39
+ /* @__PURE__ */ e(p, { when: o !== null && n === null, children: /* @__PURE__ */ e("p", { className: "text-muted-foreground text-xs", children: "This vertex could not be read." }) }),
40
+ n && /* @__PURE__ */ s(k, { children: [
41
+ /* @__PURE__ */ s("div", { children: [
42
+ /* @__PURE__ */ e("p", { className: "truncate font-medium", "data-slot": "graph-inspector-title", children: l.title === void 0 ? `#${J(n.vertex)}` : w(v(l.title)) }),
43
+ /* @__PURE__ */ s("div", { className: "mt-1 flex items-center gap-1.5", children: [
44
+ /* @__PURE__ */ e(p, { when: N !== void 0, children: /* @__PURE__ */ s(L, { className: "gap-1 text-[10px]", size: "xs", variant: "outline", children: [
45
+ /* @__PURE__ */ e(
46
+ R,
47
+ {
48
+ className: "size-2 shrink-0",
49
+ color: x.color(Math.max(0, b)),
50
+ shape: x.shape(Math.max(0, b))
51
+ }
52
+ ),
53
+ $(m, l.categories)
54
+ ] }) }),
55
+ /* @__PURE__ */ s(
56
+ z,
57
+ {
58
+ className: "h-5 gap-1 text-[10px]",
59
+ onClick: () => c.reveal(n.vertex),
60
+ size: "sm",
61
+ title: "Bring this vertex into view on the canvas",
62
+ variant: "ghost",
63
+ children: [
64
+ /* @__PURE__ */ e(A, { className: "size-3" }),
65
+ "Find on canvas"
66
+ ]
67
+ }
68
+ )
69
+ ] })
70
+ ] }),
71
+ /* @__PURE__ */ s(G, { orientation: "vertical", children: [
72
+ n.fields.map((t) => /* @__PURE__ */ s(B, { className: "gap-0.5 py-0", children: [
73
+ /* @__PURE__ */ e(F, { className: "text-xs", children: t.name }),
74
+ /* @__PURE__ */ e(M, { className: "break-all", children: w(t.value) })
75
+ ] }, t.name)),
76
+ a == null ? void 0 : a(n)
77
+ ] })
78
+ ] })
79
+ ] });
80
+ }
81
+ export {
82
+ it as GraphInspector
83
+ };
84
+ //# sourceMappingURL=graph-inspector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-inspector.js","sources":["../../src/parts/graph-inspector.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Badge,\n Button,\n cn,\n DataList,\n DataListItem,\n DataListItemLabel,\n DataListItemValue,\n Show,\n Skeleton,\n useChartCapacity,\n} from \"@kanzo-tech/ui\";\nimport { CrosshairIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { bindingOf } from \"../core/channels\";\nimport { readVertex, type VertexDetail } from \"../core/detail\";\nimport { denseOf } from \"../core/resident\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { internalsOf } from \"../react/use-graph\";\nimport { useGraphState } from \"../react/use-graph-state\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphInspectorProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /**\n * More fields for the focused vertex, drawn inside its list after the corpus's own — a\n * `DataListItem` each. A render prop rather than a table of formatters: what a product adds is\n * markup, and the part never learns what any of it means.\n */\n children?: (detail: VertexDetail) => ReactNode;\n}\n\n/** A cell, as text — a `DATE` arrives as a `Date`, and a 64-bit column as a `bigint`. */\nconst text = (value: unknown): string => {\n if (value instanceof Date) return value.toISOString().slice(0, 10);\n if (value === null || value === undefined || value === \"\") return \"—\";\n return String(value);\n};\n\ntype Answer = { vertex: bigint; detail: VertexDetail | null } | null;\n\n/**\n * **The focused vertex, fetched and laid out by the corpus's own types.** A tile carries what the\n * channels project and nothing else, so the rest of the row is read when a reader focuses it — one\n * payload tile, filtered to one `dense_id`, cancelled when the focus moves on. The fields are the\n * type's, in the order the corpus declares them; the address and the position are the canvas's.\n */\nexport function GraphInspector({ children, className, slot, ...rest }: GraphInspectorProps) {\n const api = useGraphContext();\n const focus = useGraphState((s) => s.focus);\n const corpus = useGraphState((s) => s.corpus);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const capacity = useChartCapacity();\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n const [answer, setAnswer] = useState<Answer>(null);\n\n useEffect(() => {\n const matrix = internalsOf(api).store.getSnapshot().matrix;\n if (focus === null || !corpus || !matrix) return;\n const aborter = new AbortController();\n readVertex(corpus, matrix, focus, aborter.signal).then(\n (detail) => !aborter.signal.aborted && setAnswer({ vertex: focus, detail }),\n (error: unknown) => {\n if (aborter.signal.aborted) return;\n setAnswer({ vertex: focus, detail: null });\n api.getState().options.onFailure(error instanceof Error ? error.message : String(error));\n },\n );\n return () => aborter.abort();\n }, [api, corpus, focus]);\n\n const current = answer !== null && answer.vertex === focus ? answer.detail : undefined;\n const field = (name: string | undefined) => current?.fields.find((f) => f.name === name)?.value;\n const category = bindingOf(options).category;\n const value = field(category);\n const rank = domain.indexOf(typeof value === \"bigint\" ? Number(value) : value);\n\n return (\n <div {...rest} className={cn(\"space-y-3 text-sm\", className)} data-slot={slot ?? \"graph-inspector\"}>\n <Show when={focus === null}>\n <p className=\"text-muted-foreground text-xs\">Click a vertex on the canvas to inspect it.</p>\n </Show>\n <Show when={focus !== null && current === undefined}>\n <Skeleton className=\"h-24 w-full\" />\n </Show>\n <Show when={focus !== null && current === null}>\n <p className=\"text-muted-foreground text-xs\">This vertex could not be read.</p>\n </Show>\n {current && (\n <>\n <div>\n <p className=\"truncate font-medium\" data-slot=\"graph-inspector-title\">\n {options.title === undefined ? `#${denseOf(current.vertex)}` : text(field(options.title))}\n </p>\n <div className=\"mt-1 flex items-center gap-1.5\">\n <Show when={category !== undefined}>\n <Badge className=\"gap-1 text-[10px]\" size=\"xs\" variant=\"outline\">\n <ShapeGlyph\n className=\"size-2 shrink-0\"\n color={scale.color(Math.max(0, rank))}\n shape={scale.shape(Math.max(0, rank))}\n />\n {nameOf(value, options.categories)}\n </Badge>\n </Show>\n <Button\n className=\"h-5 gap-1 text-[10px]\"\n onClick={() => api.reveal(current.vertex)}\n size=\"sm\"\n title=\"Bring this vertex into view on the canvas\"\n variant=\"ghost\"\n >\n <CrosshairIcon className=\"size-3\" />\n Find on canvas\n </Button>\n </div>\n </div>\n <DataList orientation=\"vertical\">\n {current.fields.map((f) => (\n <DataListItem className=\"gap-0.5 py-0\" key={f.name}>\n <DataListItemLabel className=\"text-xs\">{f.name}</DataListItemLabel>\n <DataListItemValue className=\"break-all\">{text(f.value)}</DataListItemValue>\n </DataListItem>\n ))}\n {children?.(current)}\n </DataList>\n </>\n )}\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAoCA;AAcO;AACL;;AAIoC;AAAqB;AAKzD;AACE;AACA;AACA;AACA;AAAkD;AAC0B;AAExE;AAEuF;AACzF;AAEmB;AAGvB;;AAC4C;AAA8C;AAK1F;AAEI;AAEA;AAGA;AAGA;AAGI;AACE;AAEA;AAEE;AAEI;AAAA;AAAC;AAAA;AACW;AAC0B;AACA;AAAA;AAAA;AAEL;AAErC;AACA;AAAC;AAAA;AACW;AAC8B;AACnC;AACC;AACE;AAER;AAAkC;AAAE;AAAA;AAAA;AAAA;AAGxC;AACF;AAEG;AAEG;AAA+C;AACS;AAE3D;AACW;AACd;AACF;AAIR;;;;"}
@@ -0,0 +1,12 @@
1
+ export type GraphLegendProps = React.ComponentProps<"div">;
2
+ /**
3
+ * **The categorical scale the canvas draws, as rows** — glyph, name and how many vertices the marks of
4
+ * that category stand for in view, then what is drawn of the whole. The rows are the domain fixed
5
+ * before any tile arrived, so a row never moves and a colour never changes hands; a category past the
6
+ * palette's capacity is Other, as it is on the canvas.
7
+ *
8
+ * Names are the root's `categories`, never this part's: a legend that renamed a category would be
9
+ * encoding one product's policy, which is the parts rule's reversal.
10
+ */
11
+ export declare function GraphLegend({ className, slot, ...rest }: GraphLegendProps): import("react").JSX.Element;
12
+ //# sourceMappingURL=graph-legend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-legend.d.ts","sourceRoot":"","sources":["../../src/parts/graph-legend.tsx"],"names":[],"mappings":"AAUA,MAAM,MAAM,gBAAgB,GAAG,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;AAI3D;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,gBAAgB,+BA6CzE"}
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { jsxs as s, jsx as a } from "react/jsx-runtime";
3
+ import { useChartCapacity as S, Show as f, cn as x } from "@kanzo-tech/ui";
4
+ import { useMemo as O } from "react";
5
+ import { nameOf as v } from "../core/categories.js";
6
+ import { bindingOf as G } from "../core/channels.js";
7
+ import { useGraphState as r } from "../react/use-graph-state.js";
8
+ import { scaleOf as j } from "../render/graph-model.js";
9
+ import { ShapeGlyph as N } from "./shape-glyph.js";
10
+ const z = (l, c) => l.length === c.length && l.every((p, n) => p === c[n]);
11
+ function E({ className: l, slot: c, ...p }) {
12
+ const n = r((e) => e.options), u = r((e) => {
13
+ var t;
14
+ return ((t = e.drawn) == null ? void 0 : t.domain) ?? e.domain;
15
+ }, z), d = r((e) => {
16
+ var t;
17
+ return ((t = e.drawn) == null ? void 0 : t.tally) ?? null;
18
+ }), w = r((e) => {
19
+ var t;
20
+ return ((t = e.drawn) == null ? void 0 : t.represented) ?? null;
21
+ }), y = r((e) => e.total), o = S(), i = O(() => j(n, o), [n, o]), g = G(n).category !== void 0, h = u.slice(0, o), b = d ? d.slice(o).reduce((e, t) => e + t, 0) : null, m = (e) => e == null ? "—" : e.toLocaleString();
22
+ return /* @__PURE__ */ s(
23
+ "div",
24
+ {
25
+ ...p,
26
+ className: x("rounded-lg border bg-card px-2.5 py-1.5 text-xs", l),
27
+ "data-slot": c ?? "graph-legend",
28
+ children: [
29
+ /* @__PURE__ */ a(f, { when: g && h.length > 0, children: /* @__PURE__ */ s("ul", { className: "space-y-1", "data-slot": "graph-legend-items", children: [
30
+ h.map((e, t) => /* @__PURE__ */ s("li", { className: "flex items-center gap-2", "data-slot": "graph-legend-item", children: [
31
+ /* @__PURE__ */ a(N, { className: "size-2.5 shrink-0", color: i.color(t), shape: i.shape(t) }),
32
+ /* @__PURE__ */ a("span", { className: "truncate", children: v(e, n.categories) }),
33
+ /* @__PURE__ */ a("span", { className: "ms-auto ps-4 text-muted-foreground tabular-nums", children: m(d ? d[t] ?? 0 : null) })
34
+ ] }, String(e))),
35
+ /* @__PURE__ */ a(f, { when: u.length > o, children: /* @__PURE__ */ s("li", { className: "flex items-center gap-2", "data-slot": "graph-legend-item", children: [
36
+ /* @__PURE__ */ a(N, { className: "size-2.5 shrink-0", color: i.color(o), shape: i.shape(o) }),
37
+ /* @__PURE__ */ a("span", { children: "Other" }),
38
+ /* @__PURE__ */ a("span", { className: "ms-auto ps-4 text-muted-foreground tabular-nums", children: m(b) })
39
+ ] }) })
40
+ ] }) }),
41
+ /* @__PURE__ */ s(
42
+ "p",
43
+ {
44
+ className: x("text-muted-foreground tabular-nums", g && h.length > 0 && "mt-1 border-t pt-1"),
45
+ "data-slot": "graph-legend-count",
46
+ children: [
47
+ m(w),
48
+ " of ",
49
+ m(y),
50
+ " drawn"
51
+ ]
52
+ }
53
+ )
54
+ ]
55
+ }
56
+ );
57
+ }
58
+ export {
59
+ E as GraphLegend
60
+ };
61
+ //# sourceMappingURL=graph-legend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-legend.js","sources":["../../src/parts/graph-legend.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn, Show, useChartCapacity } from \"@kanzo-tech/ui\";\nimport { useMemo } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { bindingOf } from \"../core/channels\";\nimport { useGraphState } from \"../react/use-graph-state\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport type GraphLegendProps = React.ComponentProps<\"div\">;\n\nconst shallow = (a: readonly unknown[], b: readonly unknown[]) => a.length === b.length && a.every((v, i) => v === b[i]);\n\n/**\n * **The categorical scale the canvas draws, as rows** — glyph, name and how many vertices the marks of\n * that category stand for in view, then what is drawn of the whole. The rows are the domain fixed\n * before any tile arrived, so a row never moves and a colour never changes hands; a category past the\n * palette's capacity is Other, as it is on the canvas.\n *\n * Names are the root's `categories`, never this part's: a legend that renamed a category would be\n * encoding one product's policy, which is the parts rule's reversal.\n */\nexport function GraphLegend({ className, slot, ...rest }: GraphLegendProps) {\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain, shallow);\n const tally = useGraphState((s) => s.drawn?.tally ?? null);\n const represented = useGraphState((s) => s.drawn?.represented ?? null);\n const total = useGraphState((s) => s.total);\n const capacity = useChartCapacity();\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n const bound = bindingOf(options).category !== undefined;\n const shown = domain.slice(0, capacity);\n const other = tally ? tally.slice(capacity).reduce((sum, n) => sum + n, 0) : null;\n const count = (n: number | null | undefined) => (n === null || n === undefined ? \"—\" : n.toLocaleString());\n\n return (\n <div\n {...rest}\n className={cn(\"rounded-lg border bg-card px-2.5 py-1.5 text-xs\", className)}\n data-slot={slot ?? \"graph-legend\"}\n >\n <Show when={bound && shown.length > 0}>\n <ul className=\"space-y-1\" data-slot=\"graph-legend-items\">\n {shown.map((value, rank) => (\n <li className=\"flex items-center gap-2\" data-slot=\"graph-legend-item\" key={String(value)}>\n <ShapeGlyph className=\"size-2.5 shrink-0\" color={scale.color(rank)} shape={scale.shape(rank)} />\n <span className=\"truncate\">{nameOf(value, options.categories)}</span>\n <span className=\"ms-auto ps-4 text-muted-foreground tabular-nums\">{count(tally ? (tally[rank] ?? 0) : null)}</span>\n </li>\n ))}\n <Show when={domain.length > capacity}>\n <li className=\"flex items-center gap-2\" data-slot=\"graph-legend-item\">\n <ShapeGlyph className=\"size-2.5 shrink-0\" color={scale.color(capacity)} shape={scale.shape(capacity)} />\n <span>Other</span>\n <span className=\"ms-auto ps-4 text-muted-foreground tabular-nums\">{count(other)}</span>\n </li>\n </Show>\n </ul>\n </Show>\n <p\n className={cn(\"text-muted-foreground tabular-nums\", bound && shown.length > 0 && \"mt-1 border-t pt-1\")}\n data-slot=\"graph-legend-count\"\n >\n {count(represented)} of {count(total)} drawn\n </p>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AAYA;AAWO;AACL;;AACoC;AAAqB;;AACtB;AAAkB;;AACZ;AAAwB;AASjE;AACE;AAAC;AAAA;AACK;AACsE;AACvD;AAEnB;AAEK;AAEG;AAA8F;AAChC;AAC8C;AAE/G;AAGG;AAAsG;AAC3F;AACqE;AAEpF;AAEJ;AACA;AAAC;AAAA;AACsG;AAC3F;AAET;AAAiB;AAAE;AAAgB;AAAE;AAAA;AAAA;AAAA;AACxC;AAAA;AAGN;;;;"}
@@ -0,0 +1,15 @@
1
+ export interface GraphToolbarProps extends React.ComponentProps<"div"> {
2
+ /** How the clusters stack. Each cluster is a `ButtonGroup` in the same orientation. */
3
+ orientation?: "horizontal" | "vertical";
4
+ }
5
+ /**
6
+ * **The commands, drawn** — the selection tools, the selection and what to do with it, the camera, and
7
+ * the live layout's transport when there is one. Each cluster is a `ButtonGroup`, because a cluster
8
+ * is a claim that its buttons do one job; the part declares no `toolbar` role, since a `role` of that
9
+ * kind promises roving focus and each button here is its own tab stop.
10
+ *
11
+ * The tools are not a `SegmentGroup`: that always has one member selected, and no tool — drag pans —
12
+ * is the posture a reader spends most of their time in.
13
+ */
14
+ export declare function GraphToolbar({ className, orientation, slot, ...rest }: GraphToolbarProps): import("react").JSX.Element;
15
+ //# sourceMappingURL=graph-toolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-toolbar.d.ts","sourceRoot":"","sources":["../../src/parts/graph-toolbar.tsx"],"names":[],"mappings":"AAoBA,MAAM,WAAW,iBAAkB,SAAQ,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;IACpE,uFAAuF;IACvF,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;CACzC;AAmBD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,WAA0B,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,iBAAiB,+BAqGvG"}
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import { jsxs as s, jsx as e } from "react/jsx-runtime";
3
+ import { ButtonGroup as h, Button as t, ButtonGroupSeparator as r, Show as g, ButtonGroupText as I, cn as y } from "@kanzo-tech/ui";
4
+ import { SquareDashedIcon as w, LassoIcon as N, ScanIcon as R, XIcon as T, PlusIcon as B, MinusIcon as G, MaximizeIcon as L, PauseIcon as P, PlayIcon as M, RotateCcwIcon as O, PinOffIcon as Z } from "lucide-react";
5
+ import { useGraphContext as j } from "../react/graph-root.js";
6
+ import { useGraphState as i } from "../react/use-graph-state.js";
7
+ const m = "rounded-lg border bg-card shadow-sm", v = 1.4, D = {
8
+ running: { pause: !0, label: "Pause the layout" },
9
+ paused: { pause: !1, label: "Resume the layout" },
10
+ settled: { pause: !1, label: "Wake the layout — it has settled" }
11
+ };
12
+ function U({ className: z, orientation: c = "horizontal", slot: x, ...S }) {
13
+ const l = j(), o = i((a) => a.corpus !== null), n = i((a) => a.tool), d = i((a) => a.selection), f = i((a) => a.total), k = i((a) => a.options.simulate ?? !1), u = i((a) => a.motion), p = i((a) => a.pinned.length), b = D[u], C = `Release ${p} pinned ${p === 1 ? "node" : "nodes"}`;
14
+ return /* @__PURE__ */ s(
15
+ "div",
16
+ {
17
+ ...S,
18
+ className: y("flex items-start gap-1.5", c === "vertical" && "flex-col items-end", z),
19
+ "data-slot": x ?? "graph-toolbar",
20
+ children: [
21
+ /* @__PURE__ */ s(h, { "aria-label": "Selection tool", className: m, orientation: c, children: [
22
+ /* @__PURE__ */ e(
23
+ t,
24
+ {
25
+ "aria-label": "Marquee",
26
+ "aria-pressed": n === "rect",
27
+ disabled: !o,
28
+ onClick: () => l.setTool(n === "rect" ? null : "rect"),
29
+ size: "icon-sm",
30
+ title: "Drag a box — or just hold Shift",
31
+ variant: n === "rect" ? "default" : "ghost",
32
+ children: /* @__PURE__ */ e(w, {})
33
+ }
34
+ ),
35
+ /* @__PURE__ */ e(r, {}),
36
+ /* @__PURE__ */ e(
37
+ t,
38
+ {
39
+ "aria-label": "Lasso",
40
+ "aria-pressed": n === "lasso",
41
+ disabled: !o,
42
+ onClick: () => l.setTool(n === "lasso" ? null : "lasso"),
43
+ size: "icon-sm",
44
+ title: "Draw a loop around a cluster",
45
+ variant: n === "lasso" ? "default" : "ghost",
46
+ children: /* @__PURE__ */ e(N, {})
47
+ }
48
+ )
49
+ ] }),
50
+ /* @__PURE__ */ e(g, { when: d !== null, children: /* @__PURE__ */ s(h, { "aria-label": "Current selection", className: m, "data-slot": "graph-toolbar-selection", children: [
51
+ /* @__PURE__ */ s(I, { className: "gap-1 ps-2 pe-2 text-xs tabular-nums", children: [
52
+ /* @__PURE__ */ e("span", { className: "font-medium text-foreground", children: d == null ? void 0 : d.vertices.length.toLocaleString() }),
53
+ f === void 0 ? " selected" : ` of ${f.toLocaleString()} selected`
54
+ ] }),
55
+ /* @__PURE__ */ e(r, {}),
56
+ /* @__PURE__ */ e(t, { "aria-label": "Frame the selection", onClick: () => l.frameSelection(), size: "icon-sm", title: "Frame the selection", variant: "ghost", children: /* @__PURE__ */ e(R, {}) }),
57
+ /* @__PURE__ */ e(t, { "aria-label": "Clear the selection", onClick: () => l.clear(), size: "icon-sm", title: "Clear the selection", variant: "ghost", children: /* @__PURE__ */ e(T, {}) })
58
+ ] }) }),
59
+ /* @__PURE__ */ s(h, { "aria-label": "Zoom and fit", className: m, orientation: c, children: [
60
+ /* @__PURE__ */ e(t, { "aria-label": "Zoom in", disabled: !o, onClick: () => l.zoomBy(v), size: "icon-sm", variant: "ghost", children: /* @__PURE__ */ e(B, {}) }),
61
+ /* @__PURE__ */ e(r, {}),
62
+ /* @__PURE__ */ e(t, { "aria-label": "Zoom out", disabled: !o, onClick: () => l.zoomBy(1 / v), size: "icon-sm", variant: "ghost", children: /* @__PURE__ */ e(G, {}) }),
63
+ /* @__PURE__ */ e(r, {}),
64
+ /* @__PURE__ */ e(t, { "aria-label": "Fit to view", disabled: !o, onClick: () => l.fit(), size: "icon-sm", variant: "ghost", children: /* @__PURE__ */ e(L, {}) })
65
+ ] }),
66
+ /* @__PURE__ */ e(g, { when: k, children: /* @__PURE__ */ s(h, { "aria-label": "Layout", className: m, orientation: c, children: [
67
+ /* @__PURE__ */ e(
68
+ t,
69
+ {
70
+ "aria-label": b.label,
71
+ "aria-pressed": u === "paused",
72
+ disabled: !o,
73
+ onClick: () => b.pause ? l.pause() : l.resume(),
74
+ size: "icon-sm",
75
+ title: b.label,
76
+ variant: u === "paused" ? "default" : "ghost",
77
+ children: u === "running" ? /* @__PURE__ */ e(P, {}) : /* @__PURE__ */ e(M, {})
78
+ }
79
+ ),
80
+ /* @__PURE__ */ e(r, {}),
81
+ /* @__PURE__ */ e(t, { "aria-label": "Re-run the layout", disabled: !o, onClick: () => l.restart(), size: "icon-sm", title: "Re-run the layout", variant: "ghost", children: /* @__PURE__ */ e(O, {}) }),
82
+ /* @__PURE__ */ s(g, { when: p > 0, children: [
83
+ /* @__PURE__ */ e(r, {}),
84
+ /* @__PURE__ */ e(t, { "aria-label": C, onClick: () => l.unpin(), size: "icon-sm", title: C, variant: "ghost", children: /* @__PURE__ */ e(Z, {}) })
85
+ ] })
86
+ ] }) })
87
+ ]
88
+ }
89
+ );
90
+ }
91
+ export {
92
+ U as GraphToolbar
93
+ };
94
+ //# sourceMappingURL=graph-toolbar.js.map
@@ -0,0 +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: \"Wake the layout — it has settled\" },\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 when there is one. 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 simulate = useGraphState((s) => s.options.simulate ?? false);\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 <Show when={simulate}>\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 </Show>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;AA+BA;AAKqE;AACpC;AACA;AAEjC;AAWO;AACL;AAWA;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;AAII;AAAA;AAAC;AAAA;AACuB;AACG;AACd;AACyC;AAC/C;AACY;AAC0B;AAEO;AAAA;AAAA;AAE9B;AAGtB;AAEE;AAAsB;AAGtB;AACF;AAEJ;AAAA;AAAA;AAGN;;;;"}
@@ -0,0 +1,33 @@
1
+ import { Graph } from '@cosmos.gl/graph';
2
+ import { Resident, VertexId } from '../core/resident';
3
+ export interface GraphOverlays {
4
+ /** The box the overlays are positioned within — the canvas' own bounds. */
5
+ hostRef: React.RefObject<HTMLDivElement | null>;
6
+ gridRef: React.RefObject<HTMLDivElement | null>;
7
+ cardRef: React.RefObject<HTMLDivElement | null>;
8
+ /** A `ref` callback for a given vertex's label. */
9
+ labelRef: (vertex: VertexId) => (element: HTMLElement | null) => void;
10
+ /** The labelled vertices, in the order the declutter pass should place them. */
11
+ setLabelOrder: (vertices: VertexId[]) => void;
12
+ setHovered: (vertex: VertexId | null) => void;
13
+ /**
14
+ * Re-register the tracked points with cosmos.gl.
15
+ *
16
+ * Call it after the graph exists and whenever the set of overlaid nodes changes. It has to be
17
+ * driven from outside because effects run in declaration order, so this hook's own effects cannot
18
+ * see a graph that a later hook is about to construct — and that construction is exactly what
19
+ * clears the registration.
20
+ */
21
+ track: () => void;
22
+ /** Ask for a repaint. Coalesced — many calls in a frame cost one. */
23
+ schedule: () => void;
24
+ }
25
+ /**
26
+ * The labels, the hover card and the grid, positioned from the renderer every frame: points are
27
+ * tracked by index through the resident map, and every overlay is held by identity.
28
+ */
29
+ export declare function useOverlays(api: {
30
+ getGraph: () => Graph | null;
31
+ getResident: () => Resident;
32
+ }): GraphOverlays;
33
+ //# sourceMappingURL=overlays.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlays.d.ts","sourceRoot":"","sources":["../../src/parts/overlays.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA8C3D,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,mDAAmD;IACnD,QAAQ,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IACtE,gFAAgF;IAChF,aAAa,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IAC9C,UAAU,EAAE,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9C;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE;IAAE,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,QAAQ,CAAA;CAAE,GAAG,aAAa,CA+N7G"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { useRef as r, useCallback as x, useEffect as S } from "react";
3
- import { whenReady as Q } from "./when-ready.js";
3
+ import { whenReady as Q } from "../render/when-ready.js";
4
4
  const T = 22, C = 14, w = 8, U = 10, V = 12, M = 3, F = (P, l, f) => Math.min(Math.max(P, l), Math.max(l, f));
5
5
  function tt(P) {
6
6
  const { getGraph: l, getResident: f } = P, O = r(null), $ = r(null), _ = r(null), A = r(/* @__PURE__ */ new Map()), L = r(/* @__PURE__ */ new Map()), m = r([]), k = r(null), E = r(null), B = r(null), p = r(0), W = x(() => {
@@ -28,19 +28,19 @@ function tt(P) {
28
28
  const [c, d] = t.spaceToScreenPosition(a);
29
29
  let s = L.current.get(n);
30
30
  s === void 0 && (s = e.offsetWidth, L.current.set(n, s));
31
- const h = c - s / 2, z = h + s, b = d - U, R = b - V, K = g !== null && (z < 0 || b < 0 || h > g.width || R > g.height), N = H.some((v) => h < v[2] && z > v[0] && R < v[3] && b > v[1]);
31
+ const h = c - s / 2, G = h + s, b = d - U, R = b - V, K = g !== null && (G < 0 || b < 0 || h > g.width || R > g.height), N = H.some((v) => h < v[2] && G > v[0] && R < v[3] && b > v[1]);
32
32
  if (K || N) {
33
33
  e.style.opacity = "0";
34
34
  continue;
35
35
  }
36
- H.push([h - M, R - M, z + M, b + M]), e.style.transform = `translate(${Math.round(h)}px, ${Math.round(R)}px)`, e.style.opacity = "1";
36
+ H.push([h - M, R - M, G + M, b + M]), e.style.transform = `translate(${Math.round(h)}px, ${Math.round(R)}px)`, e.style.opacity = "1";
37
37
  }
38
- const G = $.current;
39
- if (G) {
38
+ const z = $.current;
39
+ if (z) {
40
40
  const n = t.getZoomLevel();
41
41
  if (n > 0) {
42
42
  const e = T * n / 2 ** Math.floor(Math.log2(n)), [a, c] = t.spaceToScreenPosition([0, 0]), d = (s) => (s % e + e) % e;
43
- G.style.backgroundSize = `${e}px ${e}px`, G.style.backgroundPosition = `${d(a)}px ${d(c)}px`;
43
+ z.style.backgroundSize = `${e}px ${e}px`, z.style.backgroundPosition = `${d(a)}px ${d(c)}px`;
44
44
  }
45
45
  }
46
46
  const y = _.current, I = k.current;
@@ -93,6 +93,6 @@ function tt(P) {
93
93
  return { hostRef: O, gridRef: $, cardRef: _, labelRef: Z, setLabelOrder: j, setHovered: J, track: W, schedule: q };
94
94
  }
95
95
  export {
96
- tt as useGraphOverlays
96
+ tt as useOverlays
97
97
  };
98
- //# sourceMappingURL=use-graph-overlays.js.map
98
+ //# sourceMappingURL=overlays.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlays.js","sources":["../../src/parts/overlays.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\nimport type { Resident, VertexId } from \"../core/resident\";\nimport { whenReady } from \"../render/when-ready\";\n\n/**\n * Everything that floats over the canvas and has to keep up with it: the hub labels, the hover\n * card, and the grid's lock to the graph's own space.\n *\n * One rAF for all three, because they answer the same question — *where is the camera now* — and\n * three independent loops would read the same transform three times a frame. React never runs: the\n * overlays move by imperative style writes, and a re-render per frame would be a re-render per\n * frame.\n *\n * **An overlay is attached to a vertex, not to a slot.** Everything here outlives an answer — a\n * label element is kept across renders, a hover survives a query — so the tracked set is identities\n * and the buffer index is resolved through `Resident` at the moment of painting. Held as indices, a\n * label would keep its position and change which node it was naming the first time the resident set\n * moved, with the text and the dot disagreeing and nothing raised.\n *\n * This lived inside the canvas component among seven other concerns, and that is not a filing\n * detail: the scheduler below once kept a cancelled `requestAnimationFrame` handle in `frame`,\n * which silently disabled every overlay for the life of the page. It took a long time to find in a\n * 700-line component and would have been obvious here.\n */\n\n/**\n * Dot spacing at zoom 1. The painter keeps the on-screen spacing inside [GRID, 2·GRID).\n *\n * **Not on the barrel any more.** Both call sites that imported it wrote the same line —\n * `backgroundSize: \\`${GRID}px ${GRID}px\\`` — as the *initial* value of a style `paint` overwrites\n * on its first frame. So the number was public to spell a value this hook was about to replace, and\n * the effect below writes it instead: the element the hook owns is seeded by the hook that owns it.\n */\nconst GRID = 22;\n\n/** How far the hover card clears its node, and the margin it keeps from the canvas edge. */\nconst CARD_GAP = 14;\nconst CARD_EDGE = 8;\n\n/** How far a label floats above its node, how tall its box is, and the air it demands around it. */\nconst LABEL_LIFT = 10;\nconst LABEL_HEIGHT = 12;\nconst LABEL_GAP = 3;\n\nconst clamp = (value: number, low: number, high: number) =>\n Math.min(Math.max(value, low), Math.max(low, high));\n\nexport interface GraphOverlays {\n /** The box the overlays are positioned within — the canvas' own bounds. */\n hostRef: React.RefObject<HTMLDivElement | null>;\n gridRef: React.RefObject<HTMLDivElement | null>;\n cardRef: React.RefObject<HTMLDivElement | null>;\n /** A `ref` callback for a given vertex's label. */\n labelRef: (vertex: VertexId) => (element: HTMLElement | null) => void;\n /** The labelled vertices, in the order the declutter pass should place them. */\n setLabelOrder: (vertices: VertexId[]) => void;\n setHovered: (vertex: VertexId | null) => void;\n /**\n * Re-register the tracked points with cosmos.gl.\n *\n * Call it after the graph exists and whenever the set of overlaid nodes changes. It has to be\n * driven from outside because effects run in declaration order, so this hook's own effects cannot\n * see a graph that a later hook is about to construct — and that construction is exactly what\n * clears the registration.\n */\n track: () => void;\n /** Ask for a repaint. Coalesced — many calls in a frame cost one. */\n schedule: () => void;\n}\n\n/**\n * The labels, the hover card and the grid, positioned from the renderer every frame: points are\n * tracked by index through the resident map, and every overlay is held by identity.\n */\nexport function useOverlays(api: { getGraph: () => Graph | null; getResident: () => Resident }): GraphOverlays {\n // Both are built once by `useGraph` and are stable for the life of the component, which is what\n // makes them safe to name in the dependency arrays below.\n const { getGraph, getResident } = api;\n const hostRef = useRef<HTMLDivElement>(null);\n const gridRef = useRef<HTMLDivElement>(null);\n const cardRef = useRef<HTMLDivElement>(null);\n const labelEls = useRef(new Map<VertexId, HTMLElement>());\n /** Label widths, measured once each — reading `offsetWidth` every frame would force layout. */\n const widths = useRef(new Map<VertexId, number>());\n const order = useRef<VertexId[]>([]);\n const hoveredRef = useRef<VertexId | null>(null);\n /** The card's box, measured once per hover, for the same reason. */\n const cardSize = useRef<{ width: number; height: number } | null>(null);\n /** The canvas' own box, kept by a `ResizeObserver` — see the effect below. */\n const box = useRef<{ width: number; height: number } | null>(null);\n const frame = useRef(0);\n\n /**\n * Tell cosmos.gl which points the overlays are watching: the labelled ones, plus the hovered one.\n *\n * Registration is *not* self-maintaining. `Points.updatePositions()` ends in an argument-less\n * `trackPointsByIndices()` that clears it, and that runs whenever `isPointPositionsUpdateNeeded`\n * is set — which only `setPointPositions` does. So the registration survives every look change and\n * every slider, and is lost exactly once per graph: at construction, on the `render()` that\n * follows `setPointPositions`. Hence a caller-driven re-register rather than a one-shot.\n */\n const track = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n const hovered = hoveredRef.current;\n const watched =\n hovered === null || order.current.includes(hovered)\n ? order.current\n : [...order.current, hovered];\n // Registration is a device call like any other, and this one is *only* reached before the\n // device in the case that matters: the canvas registers its labels the moment the first\n // composition lands, which is the same commit the graph is still being built in. Dropped there, the\n // tracked map stays empty and every overlay sits at `opacity: 0` for ever.\n whenReady(graph, (ready) => ready.trackPointPositionsByIndices(getResident().indicesOf(watched)));\n }, [getGraph, getResident]);\n\n const paint = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n const resident = getResident();\n /**\n * Positions come from the tracking API, not from `getPointPositions()`.\n *\n * The difference is what gets read back per frame. `getPointPositions()` is a synchronous\n * `readPixels` of the *whole* position framebuffer — 10,000 bytes at this corpus size, plus an\n * O(n) array build — on every animation frame the simulation runs. Tracking reads a\n * `ceil(√k)²` texture for the k points that actually carry an overlay: 576 bytes for Atlas'\n * 26 labels and a hovered node. It also caches while the simulation is stopped, so a settled\n * graph costs no readback at all until something moves.\n */\n // Read lazily: with labels off and nothing hovered, the only overlay left is the grid, which\n // needs the transform and not the points.\n let positions: ReadonlyMap<number, [number, number]> | null = null;\n /**\n * Where a vertex is on screen, or `null` when it is not drawn at all.\n *\n * Two ways to be absent and they are one answer here: not resident — the query moved on and this\n * vertex is not in the current buffers — or resident and not yet tracked. Both mean *do not draw\n * an overlay for it*, and the alternative to asking is drawing it at whatever the stale index now\n * holds, which is a label on the wrong node.\n */\n const at = (vertex: VertexId): [number, number] | null => {\n const index = resident.indexOf(vertex);\n if (index === undefined) return null;\n positions ??= graph.getTrackedPointPositionsMap();\n return positions.get(index) ?? null;\n };\n\n // Placed boxes, in importance order. A label that would land on one already down is dropped\n // rather than drawn over it — an unreadable pile of overlapping names is worse than a sparser\n // set of legible ones.\n const placed: [number, number, number, number][] = [];\n const bounds = box.current;\n for (const vertex of order.current) {\n const element = labelEls.current.get(vertex);\n if (!element) continue;\n const point = at(vertex);\n if (!point) {\n element.style.opacity = \"0\";\n continue;\n }\n const [x, y] = graph.spaceToScreenPosition(point);\n let width = widths.current.get(vertex);\n if (width === undefined) {\n width = element.offsetWidth;\n widths.current.set(vertex, width);\n }\n const x1 = x - width / 2;\n const x2 = x1 + width;\n const y2 = y - LABEL_LIFT;\n const y1 = y2 - LABEL_HEIGHT;\n const offscreen =\n bounds !== null && (x2 < 0 || y2 < 0 || x1 > bounds.width || y1 > bounds.height);\n const collides = placed.some((r) => x1 < r[2] && x2 > r[0] && y1 < r[3] && y2 > r[1]);\n if (offscreen || collides) {\n element.style.opacity = \"0\";\n continue;\n }\n placed.push([x1 - LABEL_GAP, y1 - LABEL_GAP, x2 + LABEL_GAP, y2 + LABEL_GAP]);\n // Positioned at the box that was just tested, in pixels. The previous version measured a\n // rectangle here and then drew the label somewhere else — `translate(-50%, -160%)` offsets by\n // percentages of the element's own size, so the collision box sat about nine pixels below the\n // text it was meant to protect and neighbouring labels overlapped anyway.\n element.style.transform = `translate(${Math.round(x1)}px, ${Math.round(y1)}px)`;\n element.style.opacity = \"1\";\n }\n // The grid belongs to the graph's space, not to the viewport: it slides with a pan and\n // subdivides on zoom, so the spacing on screen never leaves [GRID, 2·GRID). Without that a\n // fixed grid reads as wallpaper and the canvas stops feeling like somewhere you can move.\n const grid = gridRef.current;\n if (grid) {\n const k = graph.getZoomLevel();\n if (k > 0) {\n const step = (GRID * k) / 2 ** Math.floor(Math.log2(k));\n const [ox, oy] = graph.spaceToScreenPosition([0, 0]);\n const wrap = (v: number) => ((v % step) + step) % step;\n grid.style.backgroundSize = `${step}px ${step}px`;\n grid.style.backgroundPosition = `${wrap(ox)}px ${wrap(oy)}px`;\n }\n }\n\n // The card sits above the node, flips below when the top runs out, and is held inside the\n // canvas on both axes. It used to be centred with percentage transforms, which cannot know\n // about an edge — and since this layer clips, a node near a border showed half a tooltip.\n const card = cardRef.current;\n const hovered = hoveredRef.current;\n if (card && hovered !== null && bounds) {\n const point = at(hovered);\n if (point) {\n const [x, y] = graph.spaceToScreenPosition(point);\n let size = cardSize.current;\n if (!size) {\n size = { width: card.offsetWidth, height: card.offsetHeight };\n cardSize.current = size;\n }\n const above = y - CARD_GAP - size.height;\n const below = y + CARD_GAP;\n const top = above >= CARD_EDGE ? above : below;\n card.style.transform = `translate(${Math.round(\n clamp(x - size.width / 2, CARD_EDGE, bounds.width - size.width - CARD_EDGE),\n )}px, ${Math.round(clamp(top, CARD_EDGE, bounds.height - size.height - CARD_EDGE))}px)`;\n card.style.opacity = \"1\";\n }\n }\n }, [getGraph, getResident]);\n\n // The canvas' box, measured when it changes rather than when it is read. `getBoundingClientRect()`\n // inside `paint` was one forced layout per animation frame, in a painter that caches `offsetWidth`\n // for exactly that reason.\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const observer = new ResizeObserver(([entry]) => {\n const size = entry?.contentRect;\n if (size) box.current = { width: size.width, height: size.height };\n });\n observer.observe(host);\n return () => observer.disconnect();\n }, []);\n\n /**\n * The grid's spacing at rest, written before the camera has said anything.\n *\n * `paint` sets this every frame from the live zoom, but only once there is a graph and a zoom to\n * read — and the element is mounted well before that. The two hosts that drew a grid were each\n * writing this same line inline off an exported `GRID`, which is a constant published so a call\n * site could spell the value this hook was about to overwrite. Seeding it here is the same picture\n * with the number staying where the painter that maintains it lives.\n *\n * The `backgroundImage` is not seeded: what the dots are made of is the host's decision — the\n * border colour, a gradient, whatever the surface wants — and only the *spacing* has to agree with\n * the camera.\n */\n useEffect(() => {\n const grid = gridRef.current;\n if (grid) grid.style.backgroundSize = `${GRID}px ${GRID}px`;\n }, []);\n\n const schedule = useCallback(() => {\n if (frame.current) return;\n frame.current = requestAnimationFrame(() => {\n frame.current = 0;\n paint();\n });\n }, [paint]);\n\n useEffect(\n () => () => {\n if (frame.current) cancelAnimationFrame(frame.current);\n // Clearing the handle is the whole point of this cleanup, not the cancel. `frame` doubles as\n // the \"a paint is already queued\" flag, and StrictMode runs setup → cleanup → setup on the\n // same instance, so the refs survive. Leaving a cancelled handle behind made every later\n // `schedule()` believe a frame was still pending and return early — permanently.\n frame.current = 0;\n },\n [],\n );\n\n const labelRef = useCallback(\n (vertex: VertexId) => (element: HTMLElement | null) => {\n if (element) labelEls.current.set(vertex, element);\n else labelEls.current.delete(vertex);\n },\n [],\n );\n\n const setLabelOrder = useCallback((vertices: VertexId[]) => {\n order.current = vertices;\n widths.current.clear();\n }, []);\n\n const setHovered = useCallback((vertex: VertexId | null) => {\n hoveredRef.current = vertex;\n cardSize.current = null;\n }, []);\n\n return { hostRef, gridRef, cardRef, labelRef, setLabelOrder, setHovered, track, schedule };\n}\n"],"names":[],"mappings":";;;AAoCA;AAyCO;AAGL;AAyBE;AACA;AACA;AASA;AAAgG;AAIhG;AACA;AACA;AAaA;AASA;AACE;AACA;AAE+B;AAQjC;AACE;AACA;AACA;AACA;AACE;AACA;AAAA;AAEF;AACA;AACA;AAIA;AAOA;AACE;AACA;AAAA;AAEF;AAMwB;AAK1B;AACA;AACE;AACA;AACE;AAGA;AACyD;AAC3D;AAMF;AAEA;AACE;AACA;AACE;AACA;AACA;AAIA;AAGA;AAAyC;AACmC;AAEvD;AACvB;AACF;AAMF;AACE;AACA;AACA;AACE;AACA;AAA0D;AAE5D;AACsB;AAiBtB;AACA;AAAuD;AAGzD;AACE;AAEE;AACA;AACD;AAGH;AAAA;AAEI;AAKgB;AAClB;AACA;AAGF;AAAiB;AAEb;AACmC;AACrC;AACA;AAIA;AACe;AAIf;AACmB;AAGrB;AACF;;;;"}
@@ -1,4 +1,4 @@
1
- import { Shape } from './graph-looks';
1
+ import { Shape } from '../render/graph-looks';
2
2
  /**
3
3
  * The glyph the canvas draws for a category, in the DOM — a legend key, a hover card, a preview.
4
4
  *
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape-glyph.d.ts","sourceRoot":"","sources":["../../src/parts/shape-glyph.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAE/D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,eAAgB,SAAQ,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC;IACpE,yEAAyE;IACzE,KAAK,EAAE,KAAK,CAAC;IACb;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,UAAU,CAAC,EAAE,KAAsB,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE,eAAe,+BAUrF"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as r } from "react/jsx-runtime";
2
- import { SHAPE_PATH as i } from "./graph-looks.js";
2
+ import { SHAPE_PATH as i } from "../render/graph-looks.js";
3
3
  function n({ color: o = "currentColor", shape: t, ...e }) {
4
4
  return (
5
5
  // The `viewBox` is the glyph's own box and is not a prop: the paths are authored inside a 12×12
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape-glyph.js","sources":["../../src/parts/shape-glyph.tsx"],"sourcesContent":["import { SHAPE_PATH, type Shape } from \"../render/graph-looks\";\n\n/**\n * The glyph the canvas draws for a category, in the DOM — a legend key, a hover card, a preview.\n *\n * **This replaced `SHAPE_PATH` on the barrel, and the difference is what a host is made responsible\n * for.** The path table was exported for one reason: a host drawing a legend wanted the same glyph\n * the point shader fills, because a legend drawn from a second set of shapes is a legend that\n * explains a picture nobody is looking at. What the host got was path *data*, and with it three\n * facts it had to keep in step with `graph-looks.ts` by reading a comment — that the box is twelve\n * units, that the path wants a `fill` rather than a `stroke`, and which enum index each glyph is\n * keyed by. Every one of those was silent when it drifted.\n *\n * A component carries all three and cannot disagree with itself. What is left at the call site is\n * the only thing the host actually decides: how big, and what colour.\n *\n * **No `\"use client\"`, deliberately.** It is a function of its props with no state, no effect and no\n * event handler, so it renders on a server the way any other markup does. The rest of this package\n * is client-only because a WebGL context is; a `<path>` is not.\n */\nexport interface ShapeGlyphProps extends React.SVGProps<SVGSVGElement> {\n /** Which glyph — the name `scaleOf(...).shape(ordinal)` answers with. */\n shape: Shape;\n /**\n * What to fill it with. Defaults to `currentColor`, so a glyph inside a legend row inherits the\n * row's colour and a host that is colouring by category passes `scaleOf(...).color(ordinal)`.\n */\n color?: string;\n}\n\nexport function ShapeGlyph({ color = \"currentColor\", shape, ...rest }: ShapeGlyphProps) {\n return (\n // The `viewBox` is the glyph's own box and is not a prop: the paths are authored inside a 12×12\n // square, which is the fact the host used to have to know. Size it from outside — a `className`,\n // a `width`/`height`, or `x`/`y`/`width`/`height` when this is nested inside another `<svg>`,\n // which is how a preview places a glyph at a vertex.\n <svg viewBox=\"0 0 12 12\" {...rest}>\n <path d={SHAPE_PATH[shape]} fill={color} />\n </svg>\n );\n}\n"],"names":["ShapeGlyph","color","shape","rest","jsx","SHAPE_PATH"],"mappings":";;AA8BO,SAASA,EAAW,EAAE,OAAAC,IAAQ,gBAAgB,OAAAC,GAAO,GAAGC,KAAyB;AACtF;AAAA;AAAA;AAAA;AAAA;AAAA,IAKE,gBAAAC,EAAC,OAAA,EAAI,SAAQ,aAAa,GAAGD,GAC3B,UAAA,gBAAAC,EAAC,QAAA,EAAK,GAAGC,EAAWH,CAAK,GAAG,MAAMD,GAAO,EAAA,CAC3C;AAAA;AAEJ;"}
@@ -0,0 +1,15 @@
1
+ import { ReactNode } from 'react';
2
+ import { GraphApi, UseGraphProps } from './use-graph';
3
+ /** Strict: outside a root there is nothing to answer with, and a `null` would read as an empty graph. */
4
+ export declare function useGraphContext(): GraphApi;
5
+ export interface GraphRootProviderProps {
6
+ /** The api from `useGraph`, built where the host needs to hold it. */
7
+ value: GraphApi;
8
+ children?: ReactNode;
9
+ }
10
+ export declare function GraphRootProvider({ children, value }: GraphRootProviderProps): import("react").JSX.Element;
11
+ export interface GraphRootProps extends UseGraphProps {
12
+ children?: ReactNode;
13
+ }
14
+ export declare function GraphRoot({ children, ...props }: GraphRootProps): import("react").JSX.Element;
15
+ //# sourceMappingURL=graph-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-root.d.ts","sourceRoot":"","sources":["../../src/react/graph-root.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,EAAY,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAW1E,yGAAyG;AACzG,wBAAgB,eAAe,IAAI,QAAQ,CAI1C;AAED,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,KAAK,EAAE,QAAQ,CAAC;IAChB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,sBAAsB,+BAE5E;AAED,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,cAAc,+BAG/D"}
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import { jsx as t } from "react/jsx-runtime";
3
+ import { useContext as u, createContext as a } from "react";
4
+ import { useGraph as i } from "./use-graph.js";
5
+ const e = a(null);
6
+ function c() {
7
+ const r = u(e);
8
+ if (!r) throw new Error("useGraphContext must be used inside a <GraphRoot> or <GraphRootProvider>");
9
+ return r;
10
+ }
11
+ function p({ children: r, value: o }) {
12
+ return /* @__PURE__ */ t(e.Provider, { value: o, children: r });
13
+ }
14
+ function f({ children: r, ...o }) {
15
+ const n = i(o);
16
+ return /* @__PURE__ */ t(p, { value: n, children: r });
17
+ }
18
+ export {
19
+ f as GraphRoot,
20
+ p as GraphRootProvider,
21
+ c as useGraphContext
22
+ };
23
+ //# sourceMappingURL=graph-root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-root.js","sources":["../../src/react/graph-root.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport { useGraph, type GraphApi, type UseGraphProps } from \"./use-graph\";\n\n/**\n * Ark's four, one for one — `useDialog`, `DialogRoot`, `DialogRootProvider`, `useDialogContext`:\n * `useGraph` creates the api, `GraphRoot` is `useGraph` plus the context, `GraphRootProvider` takes\n * an api a host built itself, and `useGraphContext` reads it. Neither root renders an element; the\n * canvas is a part. The api is stable, so the context never changes value: state is read through\n * `useGraphState`.\n */\nconst GraphContext = createContext<GraphApi | null>(null);\n\n/** Strict: outside a root there is nothing to answer with, and a `null` would read as an empty graph. */\nexport function useGraphContext(): GraphApi {\n const value = useContext(GraphContext);\n if (!value) throw new Error(\"useGraphContext must be used inside a <GraphRoot> or <GraphRootProvider>\");\n return value;\n}\n\nexport interface GraphRootProviderProps {\n /** The api from `useGraph`, built where the host needs to hold it. */\n value: GraphApi;\n children?: ReactNode;\n}\n\nexport function GraphRootProvider({ children, value }: GraphRootProviderProps) {\n return <GraphContext.Provider value={value}>{children}</GraphContext.Provider>;\n}\n\nexport interface GraphRootProps extends UseGraphProps {\n children?: ReactNode;\n}\n\nexport function GraphRoot({ children, ...props }: GraphRootProps) {\n const api = useGraph(props);\n return <GraphRootProvider value={api}>{children}</GraphRootProvider>;\n}\n"],"names":[],"mappings":";;;;AAYA;AAGO;AACL;AACA;AACA;AACF;AAQO;AACL;AACF;AAMO;AACL;AACA;AACF;;;;;;"}
@@ -1,5 +1,5 @@
1
- import { Look } from './graph-looks';
2
- import { Sim } from './graph-sim';
1
+ import { Look } from '../render/graph-looks';
2
+ import { Sim } from '../render/graph-sim';
3
3
  /**
4
4
  * The graph section's resolved preferences, as the two objects `useGraph` takes.
5
5
  *
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-graph-prefs.d.ts","sourceRoot":"","sources":["../../src/react/use-graph-prefs.ts"],"names":[],"mappings":"AAIA,OAAO,EAAY,KAAK,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAW,KAAK,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAGxD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,IAAI;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,CASxD"}
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import { useMemo as t } from "react";
3
3
  import { useKanzoTheme as s } from "@kanzo-tech/ui";
4
- import { lookFrom as i } from "./graph-looks.js";
5
- import { simFrom as n } from "./graph-sim.js";
6
- import { GRAPH_SECTION as f } from "./section.js";
4
+ import { lookFrom as i } from "../render/graph-looks.js";
5
+ import { simFrom as n } from "../render/graph-sim.js";
6
+ import { GRAPH_SECTION as f } from "../section.js";
7
7
  function v() {
8
8
  const o = s().sectionPrefs[f.namespace];
9
9
  return t(() => {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-graph-prefs.js","sources":["../../src/react/use-graph-prefs.ts"],"sourcesContent":["\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useKanzoTheme } from \"@kanzo-tech/ui\";\nimport { lookFrom, type Look } from \"../render/graph-looks\";\nimport { simFrom, type Sim } from \"../render/graph-sim\";\nimport { GRAPH_SECTION } from \"../section\";\n\n/**\n * The graph section's resolved preferences, as the two objects `useGraph` takes.\n *\n * The join between the provider's `sectionPrefs` and the two readers of this package's manifest.\n * Every host that drew a graph under a preferences panel wrote it — the docs' workspace and keasy's\n * discover page, the same eight lines each — and it knows nothing a host decides: the namespace,\n * the keys, the defaults and the bounds are all `GRAPH_SECTION`'s, and the resolution chain is the\n * provider's. What reaches `lookFrom`/`simFrom` is the resolved value, never the stored one.\n *\n * Needs `KanzoThemeProvider` with `GRAPH_SECTION` among its `sections`. Without it the namespace\n * resolves to nothing and both objects are the manifest's defaults.\n */\nexport function useGraphPrefs(): { look: Look; sim: Sim } {\n const resolved = useKanzoTheme().sectionPrefs[GRAPH_SECTION.namespace];\n\n return useMemo(() => {\n const values = Object.fromEntries(\n Object.entries(resolved ?? {}).map(([key, pref]) => [key, pref.value]),\n );\n return { look: lookFrom(values), sim: simFrom(values) };\n }, [resolved]);\n}\n"],"names":[],"mappings":";;;;;;AAoBO;AACL;AAEA;AACE;AAAsB;AACiD;AAEvE;AAAoD;AAExD;;;;"}
@@ -0,0 +1,11 @@
1
+ import { GraphState } from '../core/store';
2
+ /**
3
+ * **A slice of the graph's state, and a render only when that slice moves** — TanStack Store's
4
+ * `useStore(store, selector)`, over the root in context. The state changes at frame rate — a hover, a
5
+ * tile, a tick of the layout — so reading all of it would re-render a toolbar on every hover.
6
+ *
7
+ * The selection is kept while the snapshot is the same object, and while `isEqual` says the new slice
8
+ * equals the last one, so a selector may build an object as long as it passes a comparison for it.
9
+ */
10
+ export declare function useGraphState<T>(selector: (state: GraphState) => T, isEqual?: (a: T, b: T) => boolean): T;
11
+ //# sourceMappingURL=use-graph-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-graph-state.d.ts","sourceRoot":"","sources":["../../src/react/use-graph-state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,CAAC,EAAE,OAAO,GAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAmB,GAAG,CAAC,CAgBpH"}
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { useRef as c, useSyncExternalStore as l } from "react";
3
+ import { useGraphContext as i } from "./graph-root.js";
4
+ function p(s, o = Object.is) {
5
+ const n = i(), r = c(null), a = () => {
6
+ const e = n.getState(), t = r.current;
7
+ if (t && t.state === e) return t.value;
8
+ const u = s(e);
9
+ return t && o(t.value, u) ? (r.current = { state: e, value: t.value }, t.value) : (r.current = { state: e, value: u }, u);
10
+ };
11
+ return l(n.subscribe, a, a);
12
+ }
13
+ export {
14
+ p as useGraphState
15
+ };
16
+ //# sourceMappingURL=use-graph-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-graph-state.js","sources":["../../src/react/use-graph-state.ts"],"sourcesContent":["\"use client\";\n\nimport { useRef, useSyncExternalStore } from \"react\";\nimport type { GraphState } from \"../core/store\";\nimport { useGraphContext } from \"./graph-root\";\n\n/**\n * **A slice of the graph's state, and a render only when that slice moves** — TanStack Store's\n * `useStore(store, selector)`, over the root in context. The state changes at frame rate — a hover, a\n * tile, a tick of the layout — so reading all of it would re-render a toolbar on every hover.\n *\n * The selection is kept while the snapshot is the same object, and while `isEqual` says the new slice\n * equals the last one, so a selector may build an object as long as it passes a comparison for it.\n */\nexport function useGraphState<T>(selector: (state: GraphState) => T, isEqual: (a: T, b: T) => boolean = Object.is): T {\n const api = useGraphContext();\n const memo = useRef<{ state: GraphState; value: T } | null>(null);\n const read = () => {\n const state = api.getState();\n const last = memo.current;\n if (last && last.state === state) return last.value;\n const value = selector(state);\n if (last && isEqual(last.value, value)) {\n memo.current = { state, value: last.value };\n return last.value;\n }\n memo.current = { state, value };\n return value;\n };\n return useSyncExternalStore(api.subscribe, read, read);\n}\n"],"names":[],"mappings":";;;AAcO;AACL;AAGE;AAEA;AACA;AACA;AAKO;AAET;AACF;;;;"}