@kanzo-tech/graph 0.13.1 → 0.14.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.
@@ -1 +1 @@
1
- {"version":3,"file":"gesture.js","sources":["../../src/parts/gesture.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type React from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\nimport type { Resident, VertexId } from \"../core/resident\";\nimport type { Selection, SelectionSource, Tool } from \"../core/types\";\nimport { isReady } from \"../render/when-ready\";\n\n/**\n * Drawing a selection on the canvas: the marquee, the lasso, and the keys that modify them.\n *\n * The whole gesture lives here — what is being drawn, how many nodes it currently holds, and what\n * happens when the pointer comes up. The component gets handlers to spread onto an overlay and the\n * state to draw it with; it does not need to know that Shift borrows the marquee or that a hit test\n * costs a GPU readback.\n *\n * Modifiers are read at *release*, not at press, because that is when the reader has decided:\n * `Alt` removes what was drawn from the selection, `⌘`/`Ctrl` adds it, and neither replaces.\n */\nexport type Point = [number, number];\n\n/** A selection gesture in flight. The two tools differ only in what they accumulate. */\nexport type Drag = { tool: \"rect\"; from: Point; to: Point } | { tool: \"lasso\"; path: Point[] };\n\n/** Below this the hit test would be answering about a click, not a box. */\nconst MIN_RECT = 3;\n/** A raw pointer stream makes a polygon of near-duplicate vertices; the hit test walks every one. */\nconst PATH_STEP = 4;\n/** Slower than the hand, faster than the eye — and one GPU readback per gesture frame is plenty. */\nconst MEASURE_MS = 60;\n\n/** The running count rides just off the cursor, at the edge the gesture is growing from. */\nexport function cursorChip(drag: Drag | null): React.CSSProperties {\n if (!drag) return { display: \"none\" };\n const [x = 0, y = 0] = drag.tool === \"rect\" ? drag.to : (drag.path[drag.path.length - 1] ?? []);\n return { left: x + 14, top: y + 14 };\n}\n\nexport interface GraphSelectionGesture {\n /** The gesture being drawn, for the overlay to render. */\n drag: Drag | null;\n /** How many nodes it holds right now, so the number lands before the mouse does. */\n preview: number | null;\n /** The tool actually in force: the chosen one, or the marquee Shift is lending. */\n active: Tool;\n handlers: {\n onPointerDown: (event: React.PointerEvent) => void;\n onPointerMove: (event: React.PointerEvent) => void;\n onPointerUp: (event: React.PointerEvent) => void;\n onPointerCancel: () => void;\n };\n}\n\nexport interface GraphSelectionOptions {\n getGraph: () => Graph | null;\n /**\n * Who is drawn right now — the only thing that can turn a hit-test index into an identity.\n *\n * A gesture selects positions on screen, and a position is a buffer index, which the next\n * residency reuses for a different vertex. So the gesture resolves to identities here and now,\n * while the answer that produced them is still the one on screen.\n */\n getResident: () => Resident;\n /** The live selection, for the modifiers to add to or subtract from. */\n getSelection: () => Selection | null;\n commit: (vertices: Set<VertexId> | null, source: SelectionSource, label: string) => void;\n tool: Tool;\n setTool: (tool: Tool) => void;\n}\n\nconst NAME: Record<\"rect\" | \"lasso\", { source: SelectionSource; label: string }> = {\n rect: { source: \"marquee\", label: \"Marquee\" },\n lasso: { source: \"lasso\", label: \"Lasso\" },\n};\n\nexport function useGesture(options: GraphSelectionOptions): GraphSelectionGesture {\n const { commit, getGraph, getResident, getSelection, setTool, tool } = options;\n const [drag, setDrag] = useState<Drag | null>(null);\n const [preview, setPreview] = useState<number | null>(null);\n const [shift, setShift] = useState(false);\n const measured = useRef(0);\n\n const hitTest = useCallback(\n (shape: Drag): number[] => {\n const graph = getGraph();\n if (!graph) return [];\n // **Upstream's own words, on these two and on nothing else in its `.d.ts`:** *this method is\n // synchronous and must only be called when the graph is ready*. There is nothing to await in\n // a pointer handler, so the answer for a device that is not there is the honest one — a\n // gesture over a graph that has not drawn selects nothing, which is what it looks like.\n if (!isReady(graph)) return [];\n if (shape.tool === \"rect\") {\n const [[ax, ay], [bx, by]] = [shape.from, shape.to];\n if (Math.abs(bx - ax) < MIN_RECT || Math.abs(by - ay) < MIN_RECT) return [];\n return Array.from(\n graph.findPointsInRect([\n [Math.min(ax, bx), Math.min(ay, by)],\n [Math.max(ax, bx), Math.max(ay, by)],\n ]),\n );\n }\n if (shape.path.length < 3) return [];\n return Array.from(graph.findPointsInPolygon(shape.path));\n },\n [getGraph],\n );\n\n // Both hit tests are a GPU pass followed by a synchronous `readPixels`, so running one per\n // pointer event would stall the frame the gesture is being drawn into.\n const measure = useCallback(\n (shape: Drag) => {\n const now = performance.now();\n if (now - measured.current < MEASURE_MS) return;\n measured.current = now;\n setPreview(hitTest(shape).length);\n },\n [hitTest],\n );\n\n // Escape is the way out of everything, in layers: it abandons a drag in progress, then drops the\n // tool, then clears what is held. And a window that loses focus never delivers the Shift keyup,\n // which would leave the canvas unable to pan.\n useEffect(() => {\n const down = (event: KeyboardEvent) => {\n if (event.key === \"Shift\") setShift(true);\n if (event.key !== \"Escape\") return;\n setDrag((current) => {\n if (current) return null;\n if (tool) setTool(null);\n else commit(null, \"node\", \"\");\n return null;\n });\n setPreview(null);\n };\n const up = (event: KeyboardEvent) => {\n if (event.key === \"Shift\") setShift(false);\n };\n const blur = () => setShift(false);\n window.addEventListener(\"keydown\", down);\n window.addEventListener(\"keyup\", up);\n window.addEventListener(\"blur\", blur);\n return () => {\n window.removeEventListener(\"keydown\", down);\n window.removeEventListener(\"keyup\", up);\n window.removeEventListener(\"blur\", blur);\n };\n }, [commit, setTool, tool]);\n\n /** Screen coordinates inside the canvas — the space every cosmos.gl hit test speaks. */\n const at = (event: React.PointerEvent): Point => {\n const box = event.currentTarget.getBoundingClientRect();\n return [event.clientX - box.left, event.clientY - box.top];\n };\n\n const finish = (shape: Drag | null, event: React.PointerEvent) => {\n setDrag(null);\n setPreview(null);\n if (!shape) return;\n // The one line the whole gesture turns on: the hit test answers in buffer indices, and they stop\n // meaning anything the moment the resident set changes.\n const vertices = new Set(getResident().verticesAt(hitTest(shape)));\n // A gesture that caught nothing and asked for nothing is a misfire, not a request to clear —\n // clearing is what the corner's own button and Escape are for.\n if (vertices.size === 0 && !event.altKey) return;\n const { label, source } = NAME[shape.tool];\n const held = getSelection()?.vertices ?? [];\n if (event.altKey) {\n const next = new Set(held);\n for (const vertex of vertices) next.delete(vertex);\n commit(next.size > 0 ? next : null, source, label);\n } else if (event.metaKey || event.ctrlKey) {\n commit(new Set([...held, ...vertices]), source, label);\n } else {\n commit(vertices, source, label);\n }\n };\n\n const active: Tool = tool ?? (shift ? \"rect\" : null);\n\n return {\n drag,\n preview,\n active,\n handlers: {\n onPointerDown: (event) => {\n if (event.button !== 0) return;\n event.currentTarget.setPointerCapture(event.pointerId);\n const start = at(event);\n setDrag(\n active === \"rect\"\n ? { tool: \"rect\", from: start, to: start }\n : { tool: \"lasso\", path: [start] },\n );\n },\n onPointerMove: (event) => {\n if (!drag) return;\n const next = at(event);\n if (drag.tool === \"rect\") {\n const shape: Drag = { ...drag, to: next };\n setDrag(shape);\n measure(shape);\n return;\n }\n const last = drag.path[drag.path.length - 1] as Point;\n if (Math.hypot(next[0] - last[0], next[1] - last[1]) < PATH_STEP) return;\n const shape: Drag = { tool: \"lasso\", path: [...drag.path, next] };\n setDrag(shape);\n measure(shape);\n },\n onPointerUp: (event) => finish(drag, event),\n onPointerCancel: () => {\n setDrag(null);\n setPreview(null);\n },\n },\n };\n}\n"],"names":[],"mappings":";;;AA0BA;AAOO;AACL;AACA;AACA;AACF;AAkCA;AAAmF;AAC/C;AAEpC;AAEO;AACL;AAMgB;AAEZ;AACA;AAKA;AACA;AACE;AACA;AACa;AACY;AACc;AACA;AACpC;AAAA;AAGL;AACuD;AACzD;AACS;AAKK;AAEZ;AACA;AAEgC;AAClC;AACQ;AAMV;AACE;AAEE;AAOe;AAGf;AAAyC;AAG3C;AAIE;AAEuC;AACzC;AAIF;AACE;AACA;AAAyD;;AAMzD;AAGA;AAGA;AACA;AAEA;AACE;AACA;AACA;AAAiD;AAInB;AAMlC;AAAO;AACL;AACA;AACA;AACU;AAEN;AACA;AACA;AACA;AAAA;AAGmC;AAAE;AAEvC;AAEE;AACA;AACA;AACE;AACA;AAEA;AAAA;AAEF;AACA;AACA;AACA;AACa;AACf;AAC0C;AAExC;AACe;AACjB;AAAA;AAGN;;;;;"}
1
+ {"version":3,"file":"gesture.js","sources":["../../src/parts/gesture.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type React from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\nimport type { Resident, VertexId } from \"../core/resident\";\nimport type { Selection, SelectionSource, Tool } from \"../core/types\";\n\n/**\n * Drawing a selection on the canvas: the marquee, the lasso, and the keys that modify them.\n *\n * The whole gesture lives here — what is being drawn, how many nodes it currently holds, and what\n * happens when the pointer comes up. The component gets handlers to spread onto an overlay and the\n * state to draw it with; it does not need to know that Shift borrows the marquee or that a hit test\n * costs a GPU readback.\n *\n * Modifiers are read at *release*, not at press, because that is when the reader has decided:\n * `Alt` removes what was drawn from the selection, `⌘`/`Ctrl` adds it, and neither replaces.\n */\nexport type Point = [number, number];\n\n/** A selection gesture in flight. The two tools differ only in what they accumulate. */\nexport type Drag = { tool: \"rect\"; from: Point; to: Point } | { tool: \"lasso\"; path: Point[] };\n\n/** Below this the hit test would be answering about a click, not a box. */\nconst MIN_RECT = 3;\n/** A raw pointer stream makes a polygon of near-duplicate vertices; the hit test walks every one. */\nconst PATH_STEP = 4;\n/** Slower than the hand, faster than the eye — and one GPU readback per gesture frame is plenty. */\nconst MEASURE_MS = 60;\n\n/** The running count rides just off the cursor, at the edge the gesture is growing from. */\nexport function cursorChip(drag: Drag | null): React.CSSProperties {\n if (!drag) return { display: \"none\" };\n const [x = 0, y = 0] = drag.tool === \"rect\" ? drag.to : (drag.path[drag.path.length - 1] ?? []);\n return { left: x + 14, top: y + 14 };\n}\n\nexport interface GraphSelectionGesture {\n /** The gesture being drawn, for the overlay to render. */\n drag: Drag | null;\n /** How many nodes it holds right now, so the number lands before the mouse does. */\n preview: number | null;\n /** The tool actually in force: the chosen one, or the marquee Shift is lending. */\n active: Tool;\n handlers: {\n onPointerDown: (event: React.PointerEvent) => void;\n onPointerMove: (event: React.PointerEvent) => void;\n onPointerUp: (event: React.PointerEvent) => void;\n onPointerCancel: () => void;\n };\n}\n\nexport interface GraphSelectionOptions {\n getGraph: () => Graph | null;\n /**\n * Who is drawn right now — the only thing that can turn a hit-test index into an identity.\n *\n * A gesture selects positions on screen, and a position is a buffer index, which the next\n * residency reuses for a different vertex. So the gesture resolves to identities here and now,\n * while the answer that produced them is still the one on screen.\n */\n getResident: () => Resident;\n /** The live selection, for the modifiers to add to or subtract from. */\n getSelection: () => Selection | null;\n commit: (vertices: Set<VertexId> | null, source: SelectionSource, label: string) => void;\n tool: Tool;\n setTool: (tool: Tool) => void;\n}\n\nconst NAME: Record<\"rect\" | \"lasso\", { source: SelectionSource; label: string }> = {\n rect: { source: \"marquee\", label: \"Marquee\" },\n lasso: { source: \"lasso\", label: \"Lasso\" },\n};\n\nexport function useGesture(options: GraphSelectionOptions): GraphSelectionGesture {\n const { commit, getGraph, getResident, getSelection, setTool, tool } = options;\n const [drag, setDrag] = useState<Drag | null>(null);\n const [preview, setPreview] = useState<number | null>(null);\n const [shift, setShift] = useState(false);\n const measured = useRef(0);\n\n const hitTest = useCallback(\n (shape: Drag): number[] => {\n const graph = getGraph();\n if (!graph) return [];\n // Before its device cosmos.gl answers `[]` here: a gesture over a graph that has not drawn\n // selects nothing, which is what it looks like.\n if (shape.tool === \"rect\") {\n const [[ax, ay], [bx, by]] = [shape.from, shape.to];\n if (Math.abs(bx - ax) < MIN_RECT || Math.abs(by - ay) < MIN_RECT) return [];\n return Array.from(\n graph.findPointsInRect([\n [Math.min(ax, bx), Math.min(ay, by)],\n [Math.max(ax, bx), Math.max(ay, by)],\n ]),\n );\n }\n if (shape.path.length < 3) return [];\n return Array.from(graph.findPointsInPolygon(shape.path));\n },\n [getGraph],\n );\n\n // Both hit tests are a GPU pass followed by a synchronous `readPixels`, so running one per\n // pointer event would stall the frame the gesture is being drawn into.\n const measure = useCallback(\n (shape: Drag) => {\n const now = performance.now();\n if (now - measured.current < MEASURE_MS) return;\n measured.current = now;\n setPreview(hitTest(shape).length);\n },\n [hitTest],\n );\n\n // Escape is the way out of everything, in layers: it abandons a drag in progress, then drops the\n // tool, then clears what is held. And a window that loses focus never delivers the Shift keyup,\n // which would leave the canvas unable to pan.\n useEffect(() => {\n const down = (event: KeyboardEvent) => {\n if (event.key === \"Shift\") setShift(true);\n if (event.key !== \"Escape\") return;\n setDrag((current) => {\n if (current) return null;\n if (tool) setTool(null);\n else commit(null, \"node\", \"\");\n return null;\n });\n setPreview(null);\n };\n const up = (event: KeyboardEvent) => {\n if (event.key === \"Shift\") setShift(false);\n };\n const blur = () => setShift(false);\n window.addEventListener(\"keydown\", down);\n window.addEventListener(\"keyup\", up);\n window.addEventListener(\"blur\", blur);\n return () => {\n window.removeEventListener(\"keydown\", down);\n window.removeEventListener(\"keyup\", up);\n window.removeEventListener(\"blur\", blur);\n };\n }, [commit, setTool, tool]);\n\n /** Screen coordinates inside the canvas — the space every cosmos.gl hit test speaks. */\n const at = (event: React.PointerEvent): Point => {\n const box = event.currentTarget.getBoundingClientRect();\n return [event.clientX - box.left, event.clientY - box.top];\n };\n\n const finish = (shape: Drag | null, event: React.PointerEvent) => {\n setDrag(null);\n setPreview(null);\n if (!shape) return;\n // The one line the whole gesture turns on: the hit test answers in buffer indices, and they stop\n // meaning anything the moment the resident set changes.\n const vertices = new Set(getResident().verticesAt(hitTest(shape)));\n // A gesture that caught nothing and asked for nothing is a misfire, not a request to clear —\n // clearing is what the corner's own button and Escape are for.\n if (vertices.size === 0 && !event.altKey) return;\n const { label, source } = NAME[shape.tool];\n const held = getSelection()?.vertices ?? [];\n if (event.altKey) {\n const next = new Set(held);\n for (const vertex of vertices) next.delete(vertex);\n commit(next.size > 0 ? next : null, source, label);\n } else if (event.metaKey || event.ctrlKey) {\n commit(new Set([...held, ...vertices]), source, label);\n } else {\n commit(vertices, source, label);\n }\n };\n\n const active: Tool = tool ?? (shift ? \"rect\" : null);\n\n return {\n drag,\n preview,\n active,\n handlers: {\n onPointerDown: (event) => {\n if (event.button !== 0) return;\n event.currentTarget.setPointerCapture(event.pointerId);\n const start = at(event);\n setDrag(\n active === \"rect\"\n ? { tool: \"rect\", from: start, to: start }\n : { tool: \"lasso\", path: [start] },\n );\n },\n onPointerMove: (event) => {\n if (!drag) return;\n const next = at(event);\n if (drag.tool === \"rect\") {\n const shape: Drag = { ...drag, to: next };\n setDrag(shape);\n measure(shape);\n return;\n }\n const last = drag.path[drag.path.length - 1] as Point;\n if (Math.hypot(next[0] - last[0], next[1] - last[1]) < PATH_STEP) return;\n const shape: Drag = { tool: \"lasso\", path: [...drag.path, next] };\n setDrag(shape);\n measure(shape);\n },\n onPointerUp: (event) => finish(drag, event),\n onPointerCancel: () => {\n setDrag(null);\n setPreview(null);\n },\n },\n };\n}\n"],"names":[],"mappings":";;AAyBA;AAOO;AACL;AACA;AACA;AACF;AAkCA;AAAmF;AAC/C;AAEpC;AAEO;AACL;AAMgB;AAEZ;AACA;AAGA;AACE;AACA;AACa;AACY;AACc;AACA;AACpC;AAAA;AAGL;AACuD;AACzD;AACS;AAKK;AAEZ;AACA;AAEgC;AAClC;AACQ;AAMV;AACE;AAEE;AAOe;AAGf;AAAyC;AAG3C;AAIE;AAEuC;AACzC;AAIF;AACE;AACA;AAAyD;;AAMzD;AAGA;AAGA;AACA;AAEA;AACE;AACA;AACA;AAAiD;AAInB;AAMlC;AAAO;AACL;AACA;AACA;AACU;AAEN;AACA;AACA;AACA;AAAA;AAGmC;AAAE;AAEvC;AAEE;AACA;AACA;AACE;AACA;AAEA;AAAA;AAEF;AACA;AACA;AACA;AACa;AACf;AAC0C;AAExC;AACe;AACjB;AAAA;AAGN;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph-canvas.d.ts","sourceRoot":"","sources":["../../src/parts/graph-canvas.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAa1F,MAAM,WAAW,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;IACnE,oFAAoF;IACpF,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AA+BD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,gBAAgB,+BA0JnF"}
1
+ {"version":3,"file":"graph-canvas.d.ts","sourceRoot":"","sources":["../../src/parts/graph-canvas.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAc1F,MAAM,WAAW,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;IACnE,oFAAoF;IACpF,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AA+BD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,gBAAgB,+BA0InF"}
@@ -1,174 +1,185 @@
1
1
  "use client";
2
- import { jsxs as g, jsx as t } from "react/jsx-runtime";
3
- import { useThemeTick as Q, categoricalCapacity as U, Show as v, cn as V } from "@kanzo-tech/ui";
4
- import { useMemo as w, useCallback as L, useRef as X, useState as Y, useEffect as G } from "react";
5
- import { nameOf as Z } from "../core/categories.js";
6
- import { denseOf as ee } from "../core/resident.js";
7
- import { useGraphContext as te } from "../react/graph-root.js";
8
- import { internalsOf as re } from "../react/use-graph.js";
9
- import { useGraphState as f } from "../react/use-graph-state.js";
10
- import { resolveLook as oe } from "../render/graph-looks.js";
11
- import { scaleOf as ne } from "../render/graph-model.js";
12
- import { useGesture as se, cursorChip as ae } from "./gesture.js";
13
- import { useOverlays as ie } from "./overlays.js";
14
- import { ShapeGlyph as le } from "./shape-glyph.js";
15
- const D = "var(--brand-a5)";
16
- function ce(p, x, b) {
17
- const y = p.titles;
18
- if (!y || x <= 0) return [];
19
- const n = Array.from({ length: p.vertices }, (l, c) => c), u = p.sizes;
20
- u && n.sort((l, c) => (u[c] ?? 0) - (u[l] ?? 0));
21
- const i = n.slice(0, x), s = b === null ? void 0 : p.resident.indexOf(b);
22
- return s !== void 0 && !i.includes(s) && i.unshift(s), i.flatMap((l) => {
23
- const c = p.resident.at(l), o = y[l];
24
- return c === void 0 || !o ? [] : [{ vertex: c, text: o }];
2
+ import { jsxs as y, jsx as t } from "react/jsx-runtime";
3
+ import { useThemeTick as K, categoricalCapacity as P, Show as h, cn as Q } from "@kanzo-tech/ui";
4
+ import { useMemo as S, useCallback as W, useRef as U, useState as V, useEffect as N } from "react";
5
+ import { nameOf as X } from "../core/categories.js";
6
+ import { denseOf as Y } from "../core/resident.js";
7
+ import { useGraphContext as Z } from "../react/graph-root.js";
8
+ import { internalsOf as ee } from "../react/use-graph.js";
9
+ import { useGraphState as g } from "../react/use-graph-state.js";
10
+ import { resolveLook as te } from "../render/graph-looks.js";
11
+ import { scaleOf as re } from "../render/graph-model.js";
12
+ import { useGesture as ne, cursorChip as ae } from "./gesture.js";
13
+ import { useOverlays as oe } from "./overlays.js";
14
+ import { ShapeGlyph as se } from "./shape-glyph.js";
15
+ const j = "var(--brand-a5)";
16
+ function le(c, s, m) {
17
+ const u = c.titles;
18
+ if (!u || s <= 0) return [];
19
+ const n = Array.from({ length: c.vertices }, (i, r) => r), d = c.sizes;
20
+ d && n.sort((i, r) => (d[r] ?? 0) - (d[i] ?? 0));
21
+ const l = n.slice(0, s), a = m === null ? void 0 : c.resident.indexOf(m);
22
+ return a !== void 0 && !l.includes(a) && l.unshift(a), l.flatMap((i) => {
23
+ const r = c.resident.at(i), v = u[i];
24
+ return r === void 0 || !v ? [] : [{ vertex: r, text: v }];
25
25
  });
26
26
  }
27
- const de = {
27
+ const ie = {
28
28
  opening: "Opening the corpus…",
29
29
  reading: "Reading the corpus…"
30
30
  };
31
- function Re({ children: p, className: x, slot: b, ...y }) {
32
- const n = te(), { attach: u, renderer: i } = re(n), s = f((e) => e.hovered), l = f((e) => e.focus), c = f((e) => e.tool), o = f((e) => e.options), N = f((e) => {
33
- var a;
34
- return ((a = e.drawn) == null ? void 0 : a.domain) ?? e.domain;
35
- }), _ = f((e) => e.drawn === null ? de[e.status] : void 0), h = w(() => oe(o.look), [o.look]), M = L(() => {
31
+ function Ne({ children: c, className: s, slot: m, ...u }) {
32
+ const n = Z(), { attach: d, renderer: l } = ee(n), a = g((e) => e.focus), i = g((e) => e.tool), r = g((e) => e.options), v = g((e) => {
33
+ var f;
34
+ return ((f = e.drawn) == null ? void 0 : f.domain) ?? e.domain;
35
+ }), x = g((e) => e.drawn === null ? ie[e.status] : void 0), p = S(() => te(r.look), [r.look]), k = W(() => {
36
36
  var e;
37
- return ((e = i()) == null ? void 0 : e.graph) ?? null;
38
- }, [i]), E = ie({ getGraph: M, getResident: n.getResident }), { cardRef: F, gridRef: q, hostRef: R, labelRef: B, schedule: k, setHovered: T, setLabelOrder: z, track: W } = E, j = X(null), [d, J] = Y(null);
39
- G(() => {
40
- const e = j.current;
37
+ return ((e = l()) == null ? void 0 : e.graph) ?? null;
38
+ }, [l]), H = oe({ getGraph: k, getResident: n.getResident }), { cardRef: I, gridRef: $, hostRef: R, hoverAt: G, labelRef: L, schedule: b, setHovered: D, setLabelOrder: _, track: M } = H, T = U(null), [w, E] = V(null);
39
+ N(() => {
40
+ const e = T.current;
41
41
  if (e)
42
- return u(e, { onFrame: k, onComposed: J });
43
- }, [u, k]);
44
- const K = Q();
45
- G(() => {
42
+ return d(e, { onFrame: b, onComposed: E, onHover: G });
43
+ }, [d, G, b]);
44
+ const F = K();
45
+ N(() => {
46
46
  var e;
47
- (e = i()) == null || e.repaint();
48
- }, [i, K]);
49
- const O = w(
50
- () => d ? ce(d, h.labels, l) : [],
51
- [d, l, h.labels]
47
+ (e = l()) == null || e.repaint();
48
+ }, [l, F]);
49
+ const O = S(
50
+ () => w ? le(w, p.labels, a) : [],
51
+ [w, a, p.labels]
52
52
  );
53
- G(() => {
54
- z(O.map((e) => e.vertex)), T(s), W(), k();
55
- }, [s, O, k, T, z, W, h.grid]);
56
- const A = se({
57
- getGraph: M,
53
+ N(() => {
54
+ _(O.map((e) => e.vertex)), M(), b();
55
+ }, [O, b, _, M, p.grid]);
56
+ const z = ne({
57
+ getGraph: k,
58
58
  getResident: n.getResident,
59
- getSelection: L(() => n.getState().selection, [n]),
60
- commit: (e, a, C) => n.select(e ? [...e] : null, a, C),
59
+ getSelection: W(() => n.getState().selection, [n]),
60
+ commit: (e, f, J) => n.select(e ? [...e] : null, f, J),
61
61
  setTool: n.setTool,
62
- tool: c
63
- }), { active: P, drag: r, preview: S } = A, I = R.current ? U(R.current) : void 0, $ = w(() => ne(o, I), [o, I]), m = w(() => {
64
- var H;
65
- if (s === null || !d) return null;
66
- const e = d.resident.indexOf(s);
67
- if (e === void 0) return null;
68
- const a = d.categories[e] ?? 0, C = o.fill !== void 0 || o.symbol !== void 0;
69
- return {
70
- rank: a,
71
- title: ((H = d.titles) == null ? void 0 : H[e]) || `#${ee(s)}`,
72
- category: C && a < N.length ? Z(N[a], o.categories) : null
73
- };
74
- }, [d, N, s, o]);
75
- return /* @__PURE__ */ g(
62
+ tool: i
63
+ }), { active: q, drag: o, preview: C } = z, A = R.current ? P(R.current) : void 0, B = S(() => re(r, A), [r, A]);
64
+ return /* @__PURE__ */ y(
76
65
  "div",
77
66
  {
78
- ...y,
79
- className: V("relative isolate size-full overflow-hidden bg-background", x),
80
- "data-slot": b ?? "graph-canvas",
67
+ ...u,
68
+ className: Q("relative isolate size-full overflow-hidden bg-background", s),
69
+ "data-slot": m ?? "graph-canvas",
81
70
  ref: R,
82
71
  children: [
83
- /* @__PURE__ */ t("div", { className: "size-full", "data-slot": "graph-canvas-surface", ref: j }),
84
- /* @__PURE__ */ t(v, { when: h.grid, children: /* @__PURE__ */ t(
72
+ /* @__PURE__ */ t("div", { className: "size-full", "data-slot": "graph-canvas-surface", ref: T }),
73
+ /* @__PURE__ */ t(h, { when: p.grid, children: /* @__PURE__ */ t(
85
74
  "div",
86
75
  {
87
76
  className: "pointer-events-none absolute inset-0",
88
- ref: q,
77
+ ref: $,
89
78
  style: { backgroundImage: "radial-gradient(var(--border) 1px, transparent 1px)" }
90
79
  }
91
80
  ) }),
92
- /* @__PURE__ */ t(v, { when: h.vignette, children: /* @__PURE__ */ t(
81
+ /* @__PURE__ */ t(h, { when: p.vignette, children: /* @__PURE__ */ t(
93
82
  "div",
94
83
  {
95
84
  className: "pointer-events-none absolute inset-0",
96
85
  style: { background: "radial-gradient(ellipse 75% 65% at 50% 45%, transparent 30%, var(--background) 100%)" }
97
86
  }
98
87
  ) }),
99
- /* @__PURE__ */ g("div", { className: "pointer-events-none absolute inset-0 overflow-hidden", children: [
88
+ /* @__PURE__ */ y("div", { className: "pointer-events-none absolute inset-0 overflow-hidden", children: [
100
89
  O.map((e) => /* @__PURE__ */ t(
101
90
  "span",
102
91
  {
103
92
  className: "absolute top-0 left-0 whitespace-nowrap font-medium text-[10px] text-foreground leading-none opacity-0 transition-opacity [text-shadow:0_0_3px_var(--background),0_0_6px_var(--background)]",
104
- ref: B(e.vertex),
93
+ ref: L(e.vertex),
105
94
  children: e.text
106
95
  },
107
96
  String(e.vertex)
108
97
  )),
109
- m && /* @__PURE__ */ g(
110
- "div",
98
+ /* @__PURE__ */ t(
99
+ ce,
111
100
  {
112
- className: "absolute top-0 left-0 flex w-max max-w-60 items-center gap-2 rounded-lg border bg-popover px-2.5 py-2 opacity-0 shadow-lg",
113
- "data-slot": "graph-canvas-card",
114
- ref: F,
115
- children: [
116
- /* @__PURE__ */ t(le, { className: "size-2.5 shrink-0", color: $.color(m.rank), shape: $.shape(m.rank) }),
117
- /* @__PURE__ */ t("span", { className: "truncate font-medium text-popover-foreground text-sm leading-none", children: m.title }),
118
- m.category !== null && /* @__PURE__ */ t("span", { className: "text-muted-foreground text-xs", children: m.category })
119
- ]
101
+ cardRef: I,
102
+ composition: w,
103
+ domain: v,
104
+ options: r,
105
+ scale: B,
106
+ schedule: b,
107
+ setHovered: D
120
108
  }
121
109
  )
122
110
  ] }),
123
- /* @__PURE__ */ t(v, { when: P !== null, children: /* @__PURE__ */ g("div", { className: "absolute inset-0 cursor-crosshair", "data-slot": "graph-canvas-gesture", ...A.handlers, children: [
124
- r && /* @__PURE__ */ t("svg", { className: "pointer-events-none size-full", children: r.tool === "rect" ? /* @__PURE__ */ t(
111
+ /* @__PURE__ */ t(h, { when: q !== null, children: /* @__PURE__ */ y("div", { className: "absolute inset-0 cursor-crosshair", "data-slot": "graph-canvas-gesture", ...z.handlers, children: [
112
+ o && /* @__PURE__ */ t("svg", { className: "pointer-events-none size-full", children: o.tool === "rect" ? /* @__PURE__ */ t(
125
113
  "rect",
126
114
  {
127
115
  className: "stroke-primary",
128
- height: Math.abs(r.to[1] - r.from[1]),
116
+ height: Math.abs(o.to[1] - o.from[1]),
129
117
  strokeDasharray: "4 3",
130
118
  strokeWidth: 1,
131
- style: { fill: D },
132
- width: Math.abs(r.to[0] - r.from[0]),
133
- x: Math.min(r.from[0], r.to[0]),
134
- y: Math.min(r.from[1], r.to[1])
119
+ style: { fill: j },
120
+ width: Math.abs(o.to[0] - o.from[0]),
121
+ x: Math.min(o.from[0], o.to[0]),
122
+ y: Math.min(o.from[1], o.to[1])
135
123
  }
136
- ) : r.path.length > 1 && /* @__PURE__ */ t(
124
+ ) : o.path.length > 1 && /* @__PURE__ */ t(
137
125
  "polygon",
138
126
  {
139
127
  className: "stroke-primary",
140
- points: r.path.map(([e, a]) => `${e},${a}`).join(" "),
128
+ points: o.path.map(([e, f]) => `${e},${f}`).join(" "),
141
129
  strokeWidth: 1,
142
- style: { fill: D }
130
+ style: { fill: j }
143
131
  }
144
132
  ) }),
145
- /* @__PURE__ */ t(v, { when: S !== null, children: /* @__PURE__ */ g(
133
+ /* @__PURE__ */ t(h, { when: C !== null, children: /* @__PURE__ */ y(
146
134
  "span",
147
135
  {
148
136
  className: "pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums",
149
- style: ae(r),
137
+ style: ae(o),
150
138
  children: [
151
- S,
139
+ C,
152
140
  " node",
153
- S === 1 ? "" : "s"
141
+ C === 1 ? "" : "s"
154
142
  ]
155
143
  }
156
144
  ) })
157
145
  ] }) }),
158
- /* @__PURE__ */ t(v, { when: _ !== void 0, children: /* @__PURE__ */ t(
146
+ /* @__PURE__ */ t(h, { when: x !== void 0, children: /* @__PURE__ */ t(
159
147
  "p",
160
148
  {
161
149
  className: "pointer-events-none absolute inset-0 grid place-items-center text-muted-foreground text-xs",
162
150
  "data-slot": "graph-canvas-waiting",
163
- children: _
151
+ children: x
164
152
  }
165
153
  ) }),
166
- p
154
+ c
155
+ ]
156
+ }
157
+ );
158
+ }
159
+ function ce({ cardRef: c, composition: s, domain: m, options: u, scale: n, schedule: d, setHovered: l }) {
160
+ var p;
161
+ const a = g((k) => k.hovered);
162
+ N(() => {
163
+ l(a), d();
164
+ }, [a, d, l]);
165
+ const i = a === null || s == null ? void 0 : s.resident.indexOf(a);
166
+ if (a === null || !s || i === void 0) return null;
167
+ const r = s.categories[i] ?? 0, x = (u.fill !== void 0 || u.symbol !== void 0) && r < m.length ? X(m[r], u.categories) : null;
168
+ return /* @__PURE__ */ y(
169
+ "div",
170
+ {
171
+ className: "absolute top-0 left-0 flex w-max max-w-60 items-center gap-2 rounded-lg border bg-popover px-2.5 py-2 opacity-0 shadow-lg",
172
+ "data-slot": "graph-canvas-card",
173
+ ref: c,
174
+ children: [
175
+ /* @__PURE__ */ t(se, { className: "size-2.5 shrink-0", color: n.color(r), shape: n.shape(r) }),
176
+ /* @__PURE__ */ t("span", { className: "truncate font-medium text-popover-foreground text-sm leading-none", children: ((p = s.titles) == null ? void 0 : p[i]) || `#${Y(a)}` }),
177
+ /* @__PURE__ */ t(h, { when: x !== null, children: /* @__PURE__ */ t("span", { className: "text-muted-foreground text-xs", children: x }) })
167
178
  ]
168
179
  }
169
180
  );
170
181
  }
171
182
  export {
172
- Re as GraphCanvas
183
+ Ne as GraphCanvas
173
184
  };
174
185
  //# sourceMappingURL=graph-canvas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-canvas.js","sources":["../../src/parts/graph-canvas.tsx"],"sourcesContent":["\"use client\";\n\nimport { categoricalCapacity, cn, Show, useThemeTick } from \"@kanzo-tech/ui\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { denseOf, type VertexId } from \"../core/resident\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { internalsOf } from \"../react/use-graph\";\nimport { useGraphState } from \"../react/use-graph-state\";\nimport type { Composition } from \"../render/compose\";\nimport { resolveLook } from \"../render/graph-looks\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { cursorChip, useGesture } from \"./gesture\";\nimport { useOverlays } from \"./overlays\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphCanvasProps extends React.ComponentProps<\"div\"> {\n /** The chrome — a toolbar, a legend, an inspector — positioned over the surface. */\n children?: ReactNode;\n}\n\nconst WASH = \"var(--brand-a5)\";\n\ninterface Label {\n vertex: VertexId;\n text: string;\n}\n\n/** The biggest `budget` vertices by the ramp, the focused one first, each with its `title`. */\nfunction labelsOf(composition: Composition, budget: number, focus: VertexId | null): Label[] {\n const titles = composition.titles;\n if (!titles || budget <= 0) return [];\n const order = Array.from({ length: composition.vertices }, (_, i) => i);\n const ramp = composition.sizes;\n if (ramp) order.sort((a, b) => (ramp[b] ?? 0) - (ramp[a] ?? 0));\n const chosen = order.slice(0, budget);\n const focused = focus === null ? undefined : composition.resident.indexOf(focus);\n if (focused !== undefined && !chosen.includes(focused)) chosen.unshift(focused);\n return chosen.flatMap((i) => {\n const vertex = composition.resident.at(i);\n const text = titles[i];\n return vertex === undefined || !text ? [] : [{ vertex, text }];\n });\n}\n\nconst WAITING: Partial<Record<string, string>> = {\n opening: \"Opening the corpus…\",\n reading: \"Reading the corpus…\",\n};\n\n/**\n * **The element, and what the look declares over it** — the grid, the vignette, the labels and the\n * hover card are fields of `Look`, so drawing them is the canvas's; the marquee and lasso gesture is\n * too. Two layers: cosmos.gl fills the surface with a canvas of its own, so chrome parented there\n * would be a sibling inside an element the renderer resizes. The frame is `isolate`, so a host's\n * `z-index` on a child cannot escape into the page.\n *\n * The surface is never gated on the first answer: rendered behind a placeholder it deadlocks — no\n * element, no renderer, no camera, no question. Waiting is an overlay over an empty canvas.\n */\nexport function GraphCanvas({ children, className, slot, ...rest }: GraphCanvasProps) {\n const api = useGraphContext();\n const { attach, renderer } = internalsOf(api);\n const hovered = useGraphState((s) => s.hovered);\n const focus = useGraphState((s) => s.focus);\n const tool = useGraphState((s) => s.tool);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const waiting = useGraphState((s) => (s.drawn === null ? WAITING[s.status] : undefined));\n const look = useMemo(() => resolveLook(options.look), [options.look]);\n const getGraph = useCallback(() => renderer()?.graph ?? null, [renderer]);\n const overlays = useOverlays({ getGraph, getResident: api.getResident });\n const { cardRef, gridRef, hostRef, labelRef, schedule, setHovered, setLabelOrder, track } = overlays;\n const surfaceRef = useRef<HTMLDivElement>(null);\n const [composition, setComposition] = useState<Composition | null>(null);\n\n useEffect(() => {\n const surface = surfaceRef.current;\n if (!surface) return;\n return attach(surface, { onFrame: schedule, onComposed: setComposition });\n }, [attach, schedule]);\n\n const themeTick = useThemeTick();\n useEffect(() => {\n renderer()?.repaint();\n }, [renderer, themeTick]);\n\n const labels = useMemo(\n () => (composition ? labelsOf(composition, look.labels, focus) : []),\n [composition, focus, look.labels],\n );\n useEffect(() => {\n setLabelOrder(labels.map((label) => label.vertex));\n setHovered(hovered);\n track();\n schedule();\n }, [hovered, labels, schedule, setHovered, setLabelOrder, track, look.grid]);\n\n const gesture = useGesture({\n getGraph,\n getResident: api.getResident,\n getSelection: useCallback(() => api.getState().selection, [api]),\n commit: (vertices, source, label) => api.select(vertices ? [...vertices] : null, source, label),\n setTool: api.setTool,\n tool,\n });\n const { active, drag, preview } = gesture;\n\n const capacity = hostRef.current ? categoricalCapacity(hostRef.current) : undefined;\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n const card = useMemo(() => {\n if (hovered === null || !composition) return null;\n const index = composition.resident.indexOf(hovered);\n if (index === undefined) return null;\n const rank = composition.categories[index] ?? 0;\n const bound = options.fill !== undefined || options.symbol !== undefined;\n return {\n rank,\n title: composition.titles?.[index] || `#${denseOf(hovered)}`,\n category: bound && rank < domain.length ? nameOf(domain[rank], options.categories) : null,\n };\n }, [composition, domain, hovered, options]);\n\n return (\n <div\n {...rest}\n className={cn(\"relative isolate size-full overflow-hidden bg-background\", className)}\n data-slot={slot ?? \"graph-canvas\"}\n ref={hostRef}\n >\n <div className=\"size-full\" data-slot=\"graph-canvas-surface\" ref={surfaceRef} />\n <Show when={look.grid}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n ref={gridRef}\n style={{ backgroundImage: \"radial-gradient(var(--border) 1px, transparent 1px)\" }}\n />\n </Show>\n <Show when={look.vignette}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n style={{ background: \"radial-gradient(ellipse 75% 65% at 50% 45%, transparent 30%, var(--background) 100%)\" }}\n />\n </Show>\n <div className=\"pointer-events-none absolute inset-0 overflow-hidden\">\n {labels.map((label) => (\n <span\n className=\"absolute top-0 left-0 whitespace-nowrap font-medium text-[10px] text-foreground leading-none opacity-0 transition-opacity [text-shadow:0_0_3px_var(--background),0_0_6px_var(--background)]\"\n key={String(label.vertex)}\n ref={labelRef(label.vertex)}\n >\n {label.text}\n </span>\n ))}\n {card && (\n <div\n className=\"absolute top-0 left-0 flex w-max max-w-60 items-center gap-2 rounded-lg border bg-popover px-2.5 py-2 opacity-0 shadow-lg\"\n data-slot=\"graph-canvas-card\"\n ref={cardRef}\n >\n <ShapeGlyph className=\"size-2.5 shrink-0\" color={scale.color(card.rank)} shape={scale.shape(card.rank)} />\n <span className=\"truncate font-medium text-popover-foreground text-sm leading-none\">{card.title}</span>\n {card.category !== null && <span className=\"text-muted-foreground text-xs\">{card.category}</span>}\n </div>\n )}\n </div>\n <Show when={active !== null}>\n <div className=\"absolute inset-0 cursor-crosshair\" data-slot=\"graph-canvas-gesture\" {...gesture.handlers}>\n {drag && (\n <svg className=\"pointer-events-none size-full\">\n {drag.tool === \"rect\" ? (\n <rect\n className=\"stroke-primary\"\n height={Math.abs(drag.to[1] - drag.from[1])}\n strokeDasharray=\"4 3\"\n strokeWidth={1}\n style={{ fill: WASH }}\n width={Math.abs(drag.to[0] - drag.from[0])}\n x={Math.min(drag.from[0], drag.to[0])}\n y={Math.min(drag.from[1], drag.to[1])}\n />\n ) : (\n drag.path.length > 1 && (\n <polygon\n className=\"stroke-primary\"\n points={drag.path.map(([x, y]) => `${x},${y}`).join(\" \")}\n strokeWidth={1}\n style={{ fill: WASH }}\n />\n )\n )}\n </svg>\n )}\n <Show when={preview !== null}>\n <span\n className=\"pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums\"\n style={cursorChip(drag)}\n >\n {preview} node{preview === 1 ? \"\" : \"s\"}\n </span>\n </Show>\n </div>\n </Show>\n <Show when={waiting !== undefined}>\n <p\n className=\"pointer-events-none absolute inset-0 grid place-items-center text-muted-foreground text-xs\"\n data-slot=\"graph-canvas-waiting\"\n >\n {waiting}\n </p>\n </Show>\n {children}\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAqBA;AAQA;AACE;AACA;AACA;AAEA;AACA;AAEA;AAEE;AAEA;AAA6D;AAEjE;AAEA;AAAiD;AACtC;AAEX;AAYO;AACL;;AAMoC;AAAqB;;AAGtB;AAAqB;AAMxD;AACE;AACA;AACA;AAAwE;AAG1E;AACA;;AACE;AAAY;AAGd;AAAe;AACoD;AACjC;AAElC;AACE;AAGA;AAGF;AAA2B;AACzB;AACiB;AAC8C;AAC+B;AACjF;AACb;;AAOA;AACA;AACA;AACA;AAEA;AAAO;AACL;AAC0D;AAC2B;AAAA;AAIzF;AACE;AAAC;AAAA;AACK;AAC+E;AAChE;AACd;AAEL;AAA6E;AAE3E;AAAC;AAAA;AACW;AACL;AACqB;AAAsD;AAEpF;AAEE;AAAC;AAAA;AACW;AACW;AAAuF;AAEhH;AAEG;AACC;AAAC;AAAA;AACW;AAEgB;AAEnB;AAAA;AAHiB;AAK3B;AAEC;AAAC;AAAA;AACW;AACA;AACL;AAEL;AAAwG;AACR;AACN;AAAA;AAAA;AAAA;AAGhG;AAGK;AAGK;AAAC;AAAA;AACW;AACgC;AAC1B;AACH;AACE;AAC0B;AACL;AACA;AAAA;AAIpC;AAAC;AAAA;AACW;AAC6C;AAC1C;AACE;AAAK;AAI5B;AAGA;AAAC;AAAA;AACW;AACY;AAErB;AAAA;AAAQ;AAA2B;AAAA;AAAA;AAExC;AAEJ;AAEE;AAAC;AAAA;AACW;AACA;AAET;AAAA;AAEL;AACC;AAAA;AAAA;AAGP;;;;"}
1
+ {"version":3,"file":"graph-canvas.js","sources":["../../src/parts/graph-canvas.tsx"],"sourcesContent":["\"use client\";\n\nimport { categoricalCapacity, cn, Show, useThemeTick } from \"@kanzo-tech/ui\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { denseOf, type VertexId } from \"../core/resident\";\nimport type { GraphOptions } from \"../core/state\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { internalsOf } from \"../react/use-graph\";\nimport { useGraphState } from \"../react/use-graph-state\";\nimport type { Composition } from \"../render/compose\";\nimport { resolveLook } from \"../render/graph-looks\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { cursorChip, useGesture } from \"./gesture\";\nimport { useOverlays } from \"./overlays\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphCanvasProps extends React.ComponentProps<\"div\"> {\n /** The chrome — a toolbar, a legend, an inspector — positioned over the surface. */\n children?: ReactNode;\n}\n\nconst WASH = \"var(--brand-a5)\";\n\ninterface Label {\n vertex: VertexId;\n text: string;\n}\n\n/** The biggest `budget` vertices by the ramp, the focused one first, each with its `title`. */\nfunction labelsOf(composition: Composition, budget: number, focus: VertexId | null): Label[] {\n const titles = composition.titles;\n if (!titles || budget <= 0) return [];\n const order = Array.from({ length: composition.vertices }, (_, i) => i);\n const ramp = composition.sizes;\n if (ramp) order.sort((a, b) => (ramp[b] ?? 0) - (ramp[a] ?? 0));\n const chosen = order.slice(0, budget);\n const focused = focus === null ? undefined : composition.resident.indexOf(focus);\n if (focused !== undefined && !chosen.includes(focused)) chosen.unshift(focused);\n return chosen.flatMap((i) => {\n const vertex = composition.resident.at(i);\n const text = titles[i];\n return vertex === undefined || !text ? [] : [{ vertex, text }];\n });\n}\n\nconst WAITING: Partial<Record<string, string>> = {\n opening: \"Opening the corpus…\",\n reading: \"Reading the corpus…\",\n};\n\n/**\n * **The element, and what the look declares over it** — the grid, the vignette, the labels and the\n * hover card are fields of `Look`, so drawing them is the canvas's; the marquee and lasso gesture is\n * too. Two layers: cosmos.gl fills the surface with a canvas of its own, so chrome parented there\n * would be a sibling inside an element the renderer resizes. The frame is `isolate`, so a host's\n * `z-index` on a child cannot escape into the page.\n *\n * The surface is never gated on the first answer: rendered behind a placeholder it deadlocks — no\n * element, no renderer, no camera, no question. Waiting is an overlay over an empty canvas.\n */\nexport function GraphCanvas({ children, className, slot, ...rest }: GraphCanvasProps) {\n const api = useGraphContext();\n const { attach, renderer } = internalsOf(api);\n const focus = useGraphState((s) => s.focus);\n const tool = useGraphState((s) => s.tool);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const waiting = useGraphState((s) => (s.drawn === null ? WAITING[s.status] : undefined));\n const look = useMemo(() => resolveLook(options.look), [options.look]);\n const getGraph = useCallback(() => renderer()?.graph ?? null, [renderer]);\n const overlays = useOverlays({ getGraph, getResident: api.getResident });\n const { cardRef, gridRef, hostRef, hoverAt, labelRef, schedule, setHovered, setLabelOrder, track } = overlays;\n const surfaceRef = useRef<HTMLDivElement>(null);\n const [composition, setComposition] = useState<Composition | null>(null);\n\n useEffect(() => {\n const surface = surfaceRef.current;\n if (!surface) return;\n return attach(surface, { onFrame: schedule, onComposed: setComposition, onHover: hoverAt });\n }, [attach, hoverAt, schedule]);\n\n const themeTick = useThemeTick();\n useEffect(() => {\n renderer()?.repaint();\n }, [renderer, themeTick]);\n\n const labels = useMemo(\n () => (composition ? labelsOf(composition, look.labels, focus) : []),\n [composition, focus, look.labels],\n );\n useEffect(() => {\n setLabelOrder(labels.map((label) => label.vertex));\n track();\n schedule();\n }, [labels, schedule, setLabelOrder, track, look.grid]);\n\n const gesture = useGesture({\n getGraph,\n getResident: api.getResident,\n getSelection: useCallback(() => api.getState().selection, [api]),\n commit: (vertices, source, label) => api.select(vertices ? [...vertices] : null, source, label),\n setTool: api.setTool,\n tool,\n });\n const { active, drag, preview } = gesture;\n\n const capacity = hostRef.current ? categoricalCapacity(hostRef.current) : undefined;\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n\n return (\n <div\n {...rest}\n className={cn(\"relative isolate size-full overflow-hidden bg-background\", className)}\n data-slot={slot ?? \"graph-canvas\"}\n ref={hostRef}\n >\n <div className=\"size-full\" data-slot=\"graph-canvas-surface\" ref={surfaceRef} />\n <Show when={look.grid}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n ref={gridRef}\n style={{ backgroundImage: \"radial-gradient(var(--border) 1px, transparent 1px)\" }}\n />\n </Show>\n <Show when={look.vignette}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n style={{ background: \"radial-gradient(ellipse 75% 65% at 50% 45%, transparent 30%, var(--background) 100%)\" }}\n />\n </Show>\n <div className=\"pointer-events-none absolute inset-0 overflow-hidden\">\n {labels.map((label) => (\n <span\n className=\"absolute top-0 left-0 whitespace-nowrap font-medium text-[10px] text-foreground leading-none opacity-0 transition-opacity [text-shadow:0_0_3px_var(--background),0_0_6px_var(--background)]\"\n key={String(label.vertex)}\n ref={labelRef(label.vertex)}\n >\n {label.text}\n </span>\n ))}\n <HoverCard\n cardRef={cardRef}\n composition={composition}\n domain={domain}\n options={options}\n scale={scale}\n schedule={schedule}\n setHovered={setHovered}\n />\n </div>\n <Show when={active !== null}>\n <div className=\"absolute inset-0 cursor-crosshair\" data-slot=\"graph-canvas-gesture\" {...gesture.handlers}>\n {drag && (\n <svg className=\"pointer-events-none size-full\">\n {drag.tool === \"rect\" ? (\n <rect\n className=\"stroke-primary\"\n height={Math.abs(drag.to[1] - drag.from[1])}\n strokeDasharray=\"4 3\"\n strokeWidth={1}\n style={{ fill: WASH }}\n width={Math.abs(drag.to[0] - drag.from[0])}\n x={Math.min(drag.from[0], drag.to[0])}\n y={Math.min(drag.from[1], drag.to[1])}\n />\n ) : (\n drag.path.length > 1 && (\n <polygon\n className=\"stroke-primary\"\n points={drag.path.map(([x, y]) => `${x},${y}`).join(\" \")}\n strokeWidth={1}\n style={{ fill: WASH }}\n />\n )\n )}\n </svg>\n )}\n <Show when={preview !== null}>\n <span\n className=\"pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums\"\n style={cursorChip(drag)}\n >\n {preview} node{preview === 1 ? \"\" : \"s\"}\n </span>\n </Show>\n </div>\n </Show>\n <Show when={waiting !== undefined}>\n <p\n className=\"pointer-events-none absolute inset-0 grid place-items-center text-muted-foreground text-xs\"\n data-slot=\"graph-canvas-waiting\"\n >\n {waiting}\n </p>\n </Show>\n {children}\n </div>\n );\n}\n\ninterface HoverCardProps {\n cardRef: React.RefObject<HTMLDivElement | null>;\n composition: Composition | null;\n domain: readonly unknown[];\n options: GraphOptions;\n scale: ReturnType<typeof scaleOf>;\n schedule: () => void;\n setHovered: (vertex: VertexId | null) => void;\n}\n\n/** The only part that re-renders on a hover: the canvas and its labels do not. */\nfunction HoverCard({ cardRef, composition, domain, options, scale, schedule, setHovered }: HoverCardProps) {\n const hovered = useGraphState((s) => s.hovered);\n useEffect(() => {\n setHovered(hovered);\n schedule();\n }, [hovered, schedule, setHovered]);\n\n const index = hovered === null ? undefined : composition?.resident.indexOf(hovered);\n if (hovered === null || !composition || index === undefined) return null;\n const rank = composition.categories[index] ?? 0;\n const bound = options.fill !== undefined || options.symbol !== undefined;\n const category = bound && rank < domain.length ? nameOf(domain[rank], options.categories) : null;\n return (\n <div\n className=\"absolute top-0 left-0 flex w-max max-w-60 items-center gap-2 rounded-lg border bg-popover px-2.5 py-2 opacity-0 shadow-lg\"\n data-slot=\"graph-canvas-card\"\n ref={cardRef}\n >\n <ShapeGlyph className=\"size-2.5 shrink-0\" color={scale.color(rank)} shape={scale.shape(rank)} />\n <span className=\"truncate font-medium text-popover-foreground text-sm leading-none\">\n {composition.titles?.[index] || `#${denseOf(hovered)}`}\n </span>\n <Show when={category !== null}>\n <span className=\"text-muted-foreground text-xs\">{category}</span>\n </Show>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAsBA;AAQA;AACE;AACA;AACA;AAEA;AACA;AAEA;AAEE;AAEA;AAA6D;AAEjE;AAEA;AAAiD;AACtC;AAEX;AAYO;AACL;;AAKoC;AAAqB;;AAGtB;AAAqB;AAMxD;AACE;AACA;AACA;AAA0F;AAG5F;AACA;;AACE;AAAY;AAGd;AAAe;AACoD;AACjC;AAElC;AACE;AAEA;AAGF;AAA2B;AACzB;AACiB;AAC8C;AAC+B;AACjF;AACb;AAOF;AACE;AAAC;AAAA;AACK;AAC+E;AAChE;AACd;AAEL;AAA6E;AAE3E;AAAC;AAAA;AACW;AACL;AACqB;AAAsD;AAEpF;AAEE;AAAC;AAAA;AACW;AACW;AAAuF;AAEhH;AAEG;AACC;AAAC;AAAA;AACW;AAEgB;AAEnB;AAAA;AAHiB;AAK3B;AACD;AAAC;AAAA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAEJ;AAGK;AAGK;AAAC;AAAA;AACW;AACgC;AAC1B;AACH;AACE;AAC0B;AACL;AACA;AAAA;AAIpC;AAAC;AAAA;AACW;AAC6C;AAC1C;AACE;AAAK;AAI5B;AAGA;AAAC;AAAA;AACW;AACY;AAErB;AAAA;AAAQ;AAA2B;AAAA;AAAA;AAExC;AAEJ;AAEE;AAAC;AAAA;AACW;AACA;AAET;AAAA;AAEL;AACC;AAAA;AAAA;AAGP;AAaA;;AACE;AACA;AACE;AACA;AAGF;AACA;AACA;AAGA;AACE;AAAC;AAAA;AACW;AACA;AACL;AAEL;AAA8F;AAG9F;AAGA;AAAA;AAAA;AAGN;;;;"}
@@ -4,7 +4,7 @@ export interface GraphToolbarProps extends React.ComponentProps<"div"> {
4
4
  }
5
5
  /**
6
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
7
+ * the live layout's transport, which starts a layout from the points where they are. Each cluster is a `ButtonGroup`, because a cluster
8
8
  * is a claim that its buttons do one job; the part declares no `toolbar` role, since a `role` of that
9
9
  * kind promises roving focus and each button here is its own tab stop.
10
10
  *
@@ -1 +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"}
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,+BAkGvG"}
@@ -1,94 +1,94 @@
1
1
  "use client";
2
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 = {
3
+ import { ButtonGroup as h, Button as o, ButtonGroupSeparator as i, Show as C, ButtonGroupText as I, cn as k } from "@kanzo-tech/ui";
4
+ import { SquareDashedIcon as y, LassoIcon as R, ScanIcon as N, XIcon as w, PlusIcon as T, MinusIcon as B, MaximizeIcon as G, PauseIcon as L, PlayIcon as P, RotateCcwIcon as M, PinOffIcon as O } from "lucide-react";
5
+ import { useGraphContext as Z } from "../react/graph-root.js";
6
+ import { useGraphState as r } from "../react/use-graph-state.js";
7
+ const m = "rounded-lg border bg-card shadow-sm", v = 1.4, j = {
8
8
  running: { pause: !0, label: "Pause the layout" },
9
9
  paused: { pause: !1, label: "Resume the layout" },
10
- settled: { pause: !1, label: "Wake the layout — it has settled" }
10
+ settled: { pause: !1, label: "Run the layout" }
11
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"}`;
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"}`;
14
14
  return /* @__PURE__ */ s(
15
15
  "div",
16
16
  {
17
17
  ...S,
18
- className: y("flex items-start gap-1.5", c === "vertical" && "flex-col items-end", z),
18
+ className: k("flex items-start gap-1.5", c === "vertical" && "flex-col items-end", z),
19
19
  "data-slot": x ?? "graph-toolbar",
20
20
  children: [
21
21
  /* @__PURE__ */ s(h, { "aria-label": "Selection tool", className: m, orientation: c, children: [
22
22
  /* @__PURE__ */ e(
23
- t,
23
+ o,
24
24
  {
25
25
  "aria-label": "Marquee",
26
26
  "aria-pressed": n === "rect",
27
- disabled: !o,
28
- onClick: () => l.setTool(n === "rect" ? null : "rect"),
27
+ disabled: !t,
28
+ onClick: () => a.setTool(n === "rect" ? null : "rect"),
29
29
  size: "icon-sm",
30
30
  title: "Drag a box — or just hold Shift",
31
31
  variant: n === "rect" ? "default" : "ghost",
32
- children: /* @__PURE__ */ e(w, {})
32
+ children: /* @__PURE__ */ e(y, {})
33
33
  }
34
34
  ),
35
- /* @__PURE__ */ e(r, {}),
35
+ /* @__PURE__ */ e(i, {}),
36
36
  /* @__PURE__ */ e(
37
- t,
37
+ o,
38
38
  {
39
39
  "aria-label": "Lasso",
40
40
  "aria-pressed": n === "lasso",
41
- disabled: !o,
42
- onClick: () => l.setTool(n === "lasso" ? null : "lasso"),
41
+ disabled: !t,
42
+ onClick: () => a.setTool(n === "lasso" ? null : "lasso"),
43
43
  size: "icon-sm",
44
44
  title: "Draw a loop around a cluster",
45
45
  variant: n === "lasso" ? "default" : "ghost",
46
- children: /* @__PURE__ */ e(N, {})
46
+ children: /* @__PURE__ */ e(R, {})
47
47
  }
48
48
  )
49
49
  ] }),
50
- /* @__PURE__ */ e(g, { when: d !== null, children: /* @__PURE__ */ s(h, { "aria-label": "Current selection", className: m, "data-slot": "graph-toolbar-selection", children: [
50
+ /* @__PURE__ */ e(C, { when: d !== null, children: /* @__PURE__ */ s(h, { "aria-label": "Current selection", className: m, "data-slot": "graph-toolbar-selection", children: [
51
51
  /* @__PURE__ */ s(I, { className: "gap-1 ps-2 pe-2 text-xs tabular-nums", children: [
52
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`
53
+ g === void 0 ? " selected" : ` of ${g.toLocaleString()} selected`
54
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, {}) })
55
+ /* @__PURE__ */ e(i, {}),
56
+ /* @__PURE__ */ e(o, { "aria-label": "Frame the selection", onClick: () => a.frameSelection(), size: "icon-sm", title: "Frame the selection", variant: "ghost", children: /* @__PURE__ */ e(N, {}) }),
57
+ /* @__PURE__ */ e(o, { "aria-label": "Clear the selection", onClick: () => a.clear(), size: "icon-sm", title: "Clear the selection", variant: "ghost", children: /* @__PURE__ */ e(w, {}) })
58
58
  ] }) }),
59
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, {}) })
60
+ /* @__PURE__ */ e(o, { "aria-label": "Zoom in", disabled: !t, onClick: () => a.zoomBy(v), size: "icon-sm", variant: "ghost", children: /* @__PURE__ */ e(T, {}) }),
61
+ /* @__PURE__ */ e(i, {}),
62
+ /* @__PURE__ */ e(o, { "aria-label": "Zoom out", disabled: !t, onClick: () => a.zoomBy(1 / v), size: "icon-sm", variant: "ghost", children: /* @__PURE__ */ e(B, {}) }),
63
+ /* @__PURE__ */ e(i, {}),
64
+ /* @__PURE__ */ e(o, { "aria-label": "Fit to view", disabled: !t, onClick: () => a.fit(), size: "icon-sm", variant: "ghost", children: /* @__PURE__ */ e(G, {}) })
65
65
  ] }),
66
- /* @__PURE__ */ e(g, { when: k, children: /* @__PURE__ */ s(h, { "aria-label": "Layout", className: m, orientation: c, children: [
66
+ /* @__PURE__ */ s(h, { "aria-label": "Layout", className: m, orientation: c, children: [
67
67
  /* @__PURE__ */ e(
68
- t,
68
+ o,
69
69
  {
70
70
  "aria-label": b.label,
71
71
  "aria-pressed": u === "paused",
72
- disabled: !o,
73
- onClick: () => b.pause ? l.pause() : l.resume(),
72
+ disabled: !t,
73
+ onClick: () => b.pause ? a.pause() : a.resume(),
74
74
  size: "icon-sm",
75
75
  title: b.label,
76
76
  variant: u === "paused" ? "default" : "ghost",
77
- children: u === "running" ? /* @__PURE__ */ e(P, {}) : /* @__PURE__ */ e(M, {})
77
+ children: u === "running" ? /* @__PURE__ */ e(L, {}) : /* @__PURE__ */ e(P, {})
78
78
  }
79
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, {}) })
80
+ /* @__PURE__ */ e(i, {}),
81
+ /* @__PURE__ */ e(o, { "aria-label": "Re-run the layout", disabled: !t, onClick: () => a.restart(), size: "icon-sm", title: "Re-run the layout", variant: "ghost", children: /* @__PURE__ */ e(M, {}) }),
82
+ /* @__PURE__ */ s(C, { when: p > 0, children: [
83
+ /* @__PURE__ */ e(i, {}),
84
+ /* @__PURE__ */ e(o, { "aria-label": f, onClick: () => a.unpin(), size: "icon-sm", title: f, variant: "ghost", children: /* @__PURE__ */ e(O, {}) })
85
85
  ] })
86
- ] }) })
86
+ ] })
87
87
  ]
88
88
  }
89
89
  );
90
90
  }
91
91
  export {
92
- U as GraphToolbar
92
+ E as GraphToolbar
93
93
  };
94
94
  //# sourceMappingURL=graph-toolbar.js.map
@@ -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: \"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;;;;"}
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;;;;"}