@modernrelay/orbit-react 0.2.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/dist/GraphProvider-B8ITDXS0.d.ts +11 -0
  3. package/dist/chunk-7XB47YST.js +69 -0
  4. package/dist/chunk-7XB47YST.js.map +1 -0
  5. package/dist/chunk-DGFLU2SN.js +108 -0
  6. package/dist/chunk-DGFLU2SN.js.map +1 -0
  7. package/dist/chunk-JN7QCMFW.js +93 -0
  8. package/dist/chunk-JN7QCMFW.js.map +1 -0
  9. package/dist/chunk-QJ7JXQVK.js +119 -0
  10. package/dist/chunk-QJ7JXQVK.js.map +1 -0
  11. package/dist/components/ContextMenu.d.ts +73 -0
  12. package/dist/components/ContextMenu.js +367 -0
  13. package/dist/components/ContextMenu.js.map +1 -0
  14. package/dist/components/Histogram.d.ts +33 -0
  15. package/dist/components/Histogram.js +216 -0
  16. package/dist/components/Histogram.js.map +1 -0
  17. package/dist/components/Inspector.d.ts +63 -0
  18. package/dist/components/Inspector.js +246 -0
  19. package/dist/components/Inspector.js.map +1 -0
  20. package/dist/components/Legend.d.ts +45 -0
  21. package/dist/components/Legend.js +185 -0
  22. package/dist/components/Legend.js.map +1 -0
  23. package/dist/components/Minimap.d.ts +63 -0
  24. package/dist/components/Minimap.js +222 -0
  25. package/dist/components/Minimap.js.map +1 -0
  26. package/dist/components/Navigator.d.ts +90 -0
  27. package/dist/components/Navigator.js +381 -0
  28. package/dist/components/Navigator.js.map +1 -0
  29. package/dist/components/Search.d.ts +70 -0
  30. package/dist/components/Search.js +203 -0
  31. package/dist/components/Search.js.map +1 -0
  32. package/dist/components/SelectionActions.d.ts +36 -0
  33. package/dist/components/SelectionActions.js +126 -0
  34. package/dist/components/SelectionActions.js.map +1 -0
  35. package/dist/components/SimControls.d.ts +124 -0
  36. package/dist/components/SimControls.js +221 -0
  37. package/dist/components/SimControls.js.map +1 -0
  38. package/dist/components/Table.d.ts +92 -0
  39. package/dist/components/Table.js +505 -0
  40. package/dist/components/Table.js.map +1 -0
  41. package/dist/components/Timeline.d.ts +34 -0
  42. package/dist/components/Timeline.js +182 -0
  43. package/dist/components/Timeline.js.map +1 -0
  44. package/dist/components/Toolbar.d.ts +60 -0
  45. package/dist/components/Toolbar.js +220 -0
  46. package/dist/components/Toolbar.js.map +1 -0
  47. package/dist/components/Tooltip.d.ts +65 -0
  48. package/dist/components/Tooltip.js +178 -0
  49. package/dist/components/Tooltip.js.map +1 -0
  50. package/dist/index.d.ts +488 -0
  51. package/dist/index.js +827 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/shared-Bj3Gk219.d.ts +43 -0
  54. package/package.json +99 -0
@@ -0,0 +1,367 @@
1
+ import { useResolvedInstance } from '../chunk-7XB47YST.js';
2
+ import { useState, useRef, useCallback, useEffect, useSyncExternalStore } from 'react';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ var MENU_CHROME = {
6
+ minWidth: 160,
7
+ display: "flex",
8
+ flexDirection: "column",
9
+ padding: 4,
10
+ borderRadius: 8,
11
+ background: "rgba(23, 25, 32, 0.96)",
12
+ border: "1px solid rgba(255, 255, 255, 0.14)",
13
+ boxShadow: "0 8px 24px rgba(0, 0, 0, 0.5)",
14
+ color: "#e8eaf0",
15
+ font: "13px/1.4 system-ui, sans-serif",
16
+ zIndex: 10
17
+ };
18
+ var HEADING_STYLE = {
19
+ padding: "4px 10px",
20
+ fontSize: 11,
21
+ opacity: 0.65,
22
+ whiteSpace: "nowrap",
23
+ overflow: "hidden",
24
+ textOverflow: "ellipsis",
25
+ maxWidth: 240
26
+ };
27
+ var ITEM_STYLE = {
28
+ appearance: "none",
29
+ display: "block",
30
+ width: "100%",
31
+ textAlign: "left",
32
+ background: "transparent",
33
+ border: "none",
34
+ borderRadius: 6,
35
+ color: "inherit",
36
+ cursor: "pointer",
37
+ font: "inherit",
38
+ padding: "6px 10px"
39
+ };
40
+ function nodeLabelText(node) {
41
+ const attrs = node.attrs;
42
+ const label = attrs?.["label"];
43
+ return label === void 0 || label === null ? String(node.id) : String(label);
44
+ }
45
+ function legacyCopy(text) {
46
+ if (typeof document === "undefined") return;
47
+ const ta = document.createElement("textarea");
48
+ ta.value = text;
49
+ ta.setAttribute("readonly", "");
50
+ ta.style.position = "fixed";
51
+ ta.style.left = "-9999px";
52
+ document.body.appendChild(ta);
53
+ ta.select();
54
+ try {
55
+ document.execCommand("copy");
56
+ } catch {
57
+ }
58
+ ta.remove();
59
+ }
60
+ function copyText(text) {
61
+ const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
62
+ if (clipboard !== void 0 && typeof clipboard.writeText === "function") {
63
+ void clipboard.writeText(text).catch(() => {
64
+ legacyCopy(text);
65
+ });
66
+ return;
67
+ }
68
+ legacyCopy(text);
69
+ }
70
+ function defaultNodeItems(instance, node, pendingExpansions, pathPair) {
71
+ const pinned = instance.store.getState().pins.has(node.id);
72
+ const expanding = pendingExpansions.has(node.id);
73
+ const folded = instance.getFold(node.id) !== null;
74
+ const items = [
75
+ {
76
+ id: "focus",
77
+ label: "Focus",
78
+ onSelect: () => {
79
+ instance.focusNode(node.id);
80
+ }
81
+ },
82
+ {
83
+ id: "select-neighbors",
84
+ label: "Select neighbors",
85
+ onSelect: () => {
86
+ instance.selectNeighbors(node.id);
87
+ }
88
+ },
89
+ {
90
+ id: "expand",
91
+ label: expanding ? "Expanding\u2026" : "Expand neighbors",
92
+ disabled: expanding,
93
+ onSelect: () => {
94
+ void instance.expandNode(node.id).catch(() => {
95
+ });
96
+ }
97
+ },
98
+ {
99
+ id: "isolate",
100
+ label: "Isolate",
101
+ onSelect: () => {
102
+ instance.applyHostUpdate({ subgraph: { seedIds: [node.id], hops: 1 } });
103
+ }
104
+ },
105
+ folded ? {
106
+ id: "unfold",
107
+ label: "Expand neighborhood",
108
+ onSelect: () => {
109
+ instance.unfoldNode(node.id);
110
+ }
111
+ } : {
112
+ id: "fold",
113
+ label: "Collapse neighborhood",
114
+ onSelect: () => {
115
+ instance.foldNode(node.id);
116
+ }
117
+ },
118
+ pinned ? {
119
+ id: "unpin",
120
+ label: "Unpin",
121
+ onSelect: () => {
122
+ instance.unpinNode(node.id);
123
+ }
124
+ } : {
125
+ id: "pin",
126
+ label: "Pin",
127
+ onSelect: () => {
128
+ instance.pinNode(node.id);
129
+ }
130
+ },
131
+ {
132
+ id: "hide",
133
+ label: "Hide",
134
+ onSelect: () => {
135
+ instance.hideNodes([node.id]);
136
+ }
137
+ },
138
+ {
139
+ id: "copy-id",
140
+ label: "Copy id",
141
+ onSelect: () => {
142
+ copyText(String(node.id));
143
+ }
144
+ }
145
+ ];
146
+ if (pathPair !== null) {
147
+ items.push({
148
+ id: "find-path-from",
149
+ label: "Find path from here",
150
+ onSelect: () => {
151
+ pathPair.setAnchor(node.id);
152
+ }
153
+ });
154
+ if (pathPair.anchorId !== null && pathPair.anchorId !== node.id) {
155
+ const anchorId = pathPair.anchorId;
156
+ items.push({
157
+ id: "find-path-to",
158
+ label: "Find path to here",
159
+ onSelect: () => {
160
+ pathPair.setAnchor(null);
161
+ pathPair.onFindPath(anchorId, node.id);
162
+ }
163
+ });
164
+ }
165
+ }
166
+ return items;
167
+ }
168
+ function defaultBackgroundItems(instance, scope) {
169
+ const items = [
170
+ {
171
+ id: "select-all",
172
+ label: "Select all",
173
+ onSelect: () => {
174
+ instance.selectAll();
175
+ }
176
+ },
177
+ {
178
+ id: "clear-selection",
179
+ label: "Clear selection",
180
+ onSelect: () => {
181
+ instance.clearSelection();
182
+ }
183
+ },
184
+ {
185
+ id: "fit-view",
186
+ label: "Fit view",
187
+ onSelect: () => {
188
+ instance.fitView();
189
+ }
190
+ }
191
+ ];
192
+ if (scope !== null) {
193
+ items.push({
194
+ id: "reset-isolation",
195
+ label: "Reset isolation",
196
+ onSelect: () => {
197
+ instance.resetIsolation();
198
+ }
199
+ });
200
+ }
201
+ return items;
202
+ }
203
+ var selectScope = (s) => s.scope;
204
+ var selectPendingExpansions = (s) => s.pendingExpansions;
205
+ function useInstanceStore(instance, selector) {
206
+ const subscribe = useCallback(
207
+ (onChange) => instance.store.subscribe(onChange),
208
+ [instance]
209
+ );
210
+ const selectorRef = useRef(selector);
211
+ selectorRef.current = selector;
212
+ const getSnapshot = useCallback(() => selectorRef.current(instance.store.getState()), [instance]);
213
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
214
+ }
215
+ function GraphContextMenu(props) {
216
+ const instance = useResolvedInstance(props.instance, "<GraphContextMenu>");
217
+ const scope = useInstanceStore(instance, selectScope);
218
+ const pendingExpansions = useInstanceStore(instance, selectPendingExpansions);
219
+ const [open, setOpen] = useState(null);
220
+ const [activeIndex, setActiveIndex] = useState(0);
221
+ const rootRef = useRef(null);
222
+ const itemRefs = useRef([]);
223
+ const restoreFocusRef = useRef(null);
224
+ const pathAnchorRef = useRef(null);
225
+ const close = useCallback((restoreFocus) => {
226
+ setOpen(null);
227
+ const el = restoreFocusRef.current;
228
+ restoreFocusRef.current = null;
229
+ if (restoreFocus && el !== null && el.isConnected) el.focus();
230
+ }, []);
231
+ useEffect(() => {
232
+ const off = instance.on("contextMenu", (payload) => {
233
+ restoreFocusRef.current = typeof document !== "undefined" && document.activeElement instanceof HTMLElement ? document.activeElement : null;
234
+ setOpen({ target: payload.target, screen: payload.screen });
235
+ setActiveIndex(0);
236
+ });
237
+ return off;
238
+ }, [instance]);
239
+ useEffect(() => {
240
+ if (open === null) return void 0;
241
+ const onDocPointerDown = (e) => {
242
+ const root = rootRef.current;
243
+ if (root !== null && e.target instanceof Node && root.contains(e.target)) return;
244
+ close(false);
245
+ };
246
+ const onWindowBlur = () => {
247
+ close(false);
248
+ };
249
+ document.addEventListener("pointerdown", onDocPointerDown, true);
250
+ window.addEventListener("blur", onWindowBlur);
251
+ return () => {
252
+ document.removeEventListener("pointerdown", onDocPointerDown, true);
253
+ window.removeEventListener("blur", onWindowBlur);
254
+ };
255
+ }, [open, close]);
256
+ useEffect(() => {
257
+ if (open === null) return;
258
+ const el = itemRefs.current[activeIndex] ?? itemRefs.current[0] ?? rootRef.current;
259
+ el?.focus();
260
+ }, [open, activeIndex]);
261
+ if (open === null) return null;
262
+ const onFindPath = props.onFindPath;
263
+ const defaultItems = open.target.kind === "node" ? defaultNodeItems(
264
+ instance,
265
+ open.target.node,
266
+ pendingExpansions,
267
+ onFindPath !== void 0 ? {
268
+ onFindPath,
269
+ anchorId: pathAnchorRef.current,
270
+ setAnchor: (id) => {
271
+ pathAnchorRef.current = id;
272
+ }
273
+ } : null
274
+ ) : defaultBackgroundItems(instance, scope);
275
+ const items = props.renderItems !== void 0 ? props.renderItems({ target: open.target, defaultItems }) : defaultItems;
276
+ const heading = open.target.kind === "node" ? nodeLabelText(open.target.node) : null;
277
+ const activate = (item) => {
278
+ close(true);
279
+ item.onSelect();
280
+ };
281
+ const onKeyDown = (e) => {
282
+ const count = items.length;
283
+ switch (e.key) {
284
+ case "ArrowDown":
285
+ e.preventDefault();
286
+ setActiveIndex((i) => count === 0 ? 0 : (i + 1) % count);
287
+ break;
288
+ case "ArrowUp":
289
+ e.preventDefault();
290
+ setActiveIndex((i) => count === 0 ? 0 : (i - 1 + count) % count);
291
+ break;
292
+ case "Home":
293
+ e.preventDefault();
294
+ setActiveIndex(0);
295
+ break;
296
+ case "End":
297
+ e.preventDefault();
298
+ setActiveIndex(count === 0 ? 0 : count - 1);
299
+ break;
300
+ case "Enter":
301
+ case " ": {
302
+ e.preventDefault();
303
+ const item = items[activeIndex];
304
+ if (item !== void 0 && item.disabled !== true) activate(item);
305
+ break;
306
+ }
307
+ case "Escape":
308
+ e.preventDefault();
309
+ close(true);
310
+ break;
311
+ case "Tab":
312
+ e.preventDefault();
313
+ break;
314
+ }
315
+ };
316
+ const positionStyle = {
317
+ position: "absolute",
318
+ left: open.screen[0],
319
+ top: open.screen[1]
320
+ };
321
+ return /* @__PURE__ */ jsxs(
322
+ "div",
323
+ {
324
+ ref: rootRef,
325
+ "data-orbit-context-menu": "",
326
+ role: "menu",
327
+ "aria-label": heading ?? "Graph",
328
+ tabIndex: -1,
329
+ className: props.className,
330
+ style: props.className !== void 0 ? { ...positionStyle, ...props.style } : { ...MENU_CHROME, ...positionStyle, ...props.style },
331
+ onKeyDown,
332
+ children: [
333
+ heading !== null ? (
334
+ // Target-derived text renders as a TEXT NODE (§14/§15 injection ban).
335
+ /* @__PURE__ */ jsx("div", { "data-orbit-context-menu-heading": "", role: "presentation", style: HEADING_STYLE, children: heading })
336
+ ) : null,
337
+ items.map((item, index) => /* @__PURE__ */ jsx(
338
+ "button",
339
+ {
340
+ ref: (el) => {
341
+ itemRefs.current[index] = el;
342
+ },
343
+ type: "button",
344
+ role: "menuitem",
345
+ "data-orbit-context-menu-item": item.id,
346
+ tabIndex: index === activeIndex ? 0 : -1,
347
+ disabled: item.disabled === true,
348
+ className: props.itemClassName,
349
+ style: props.itemClassName !== void 0 ? void 0 : ITEM_STYLE,
350
+ onClick: () => {
351
+ if (item.disabled !== true) activate(item);
352
+ },
353
+ onPointerEnter: () => {
354
+ setActiveIndex(index);
355
+ },
356
+ children: item.label
357
+ },
358
+ item.id
359
+ ))
360
+ ]
361
+ }
362
+ );
363
+ }
364
+
365
+ export { GraphContextMenu };
366
+ //# sourceMappingURL=ContextMenu.js.map
367
+ //# sourceMappingURL=ContextMenu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/ContextMenu/index.tsx"],"names":[],"mappings":"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","file":"ContextMenu.js","sourcesContent":["/**\n * <GraphContextMenu> — §16.4 right-click / long-press menu (S7-T08).\n *\n * Listens to the instance's typed 'contextMenu' event (the ONLY channel — the\n * instance has no openContextMenu method) and renders a portal-free,\n * absolutely-positioned menu at the event's container-relative screen\n * coordinates, so it must be composed inside the <Graph> container (or any\n * positioned ancestor sharing the canvas coordinate space).\n *\n * Fully keyboard reachable: the menu opens focused on its first item,\n * ArrowUp/ArrowDown rove (wrapping), Enter/Space activates, Escape closes and\n * returns focus to the previously focused element; it also closes on outside\n * pointerdown and window blur. All target-derived text (the node label\n * heading) renders as TEXT NODES — never markup.\n *\n * §16.2 path pair (S12-T08): with `onFindPath` provided, node menus carry\n * 'Find path from here' / 'Find path to here'. The pending 'from' anchor is\n * stashed locally in the component (session-local); completing the pair\n * fires `onFindPath(anchorId, targetId)` and clears the anchor.\n */\n\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';\nimport type { CSSProperties, KeyboardEvent as ReactKeyboardEvent, ReactElement } from 'react';\nimport type { GraphNode, GraphStoreState, NodeId, SubgraphSpec } from '@modernrelay/orbit-core';\nimport type { AnyGraphInstance } from '../../GraphProvider';\nimport { useResolvedInstance } from '../shared';\n\n/** The typed 'contextMenu' event target (generics erased like AnyGraphInstance). */\nexport type GraphContextMenuTarget =\n | { kind: 'node'; node: GraphNode<any> }\n | { kind: 'background' };\n\nexport interface GraphContextMenuItem {\n /** Stable key; default items use 'focus' | 'select-neighbors' | 'expand' |\n * 'isolate' | 'fold' | 'unfold' | 'pin' | 'unpin' | 'hide' | 'copy-id' |\n * 'find-path-from' | 'find-path-to' | 'select-all' | 'clear-selection' |\n * 'fit-view' | 'reset-isolation'. */\n id: string;\n /** Rendered as a text node. */\n label: string;\n disabled?: boolean;\n onSelect(): void;\n}\n\n/** Pinned-interface alias (§16.4 render-prop item type). */\nexport type MenuItem = GraphContextMenuItem;\n\nexport interface GraphContextMenuProps {\n /** Explicit instance; defaults to the ambient <Graph>/<GraphProvider> one. */\n instance?: AnyGraphInstance;\n /** Class hook for the menu root; providing it drops the default chrome\n * (positioning is always applied). */\n className?: string;\n style?: CSSProperties;\n /** Class hook for each item; providing it drops the default item styles. */\n itemClassName?: string;\n /** Render-prop REPLACEMENT for the item list; receives the default items\n * so extension is `[...defaultItems, mine]`. */\n renderItems?: (ctx: {\n target: GraphContextMenuTarget;\n defaultItems: readonly GraphContextMenuItem[];\n }) => readonly GraphContextMenuItem[];\n /**\n * §16.2 path pair (S12-T08): providing this adds 'Find path from here' /\n * 'Find path to here' to node menus. 'From here' stashes the anchor node\n * id locally (component state — session-local, never serialized); 'to\n * here' appears once an anchor exists on a DIFFERENT node, fires\n * `(anchorId, targetId)`, and clears the anchor. The component never\n * resolves paths itself — wire this to the ref API's findPath.\n */\n onFindPath?: (sourceId: NodeId, targetId: NodeId) => void;\n}\n\ninterface OpenState {\n target: GraphContextMenuTarget;\n /** Container-relative CSS px (event contract §15). */\n screen: readonly [number, number];\n}\n\n// --- default dark-theme styling (dropped when class hooks are provided) ---\n\nconst MENU_CHROME: CSSProperties = {\n minWidth: 160,\n display: 'flex',\n flexDirection: 'column',\n padding: 4,\n borderRadius: 8,\n background: 'rgba(23, 25, 32, 0.96)',\n border: '1px solid rgba(255, 255, 255, 0.14)',\n boxShadow: '0 8px 24px rgba(0, 0, 0, 0.5)',\n color: '#e8eaf0',\n font: '13px/1.4 system-ui, sans-serif',\n zIndex: 10,\n};\n\nconst HEADING_STYLE: CSSProperties = {\n padding: '4px 10px',\n fontSize: 11,\n opacity: 0.65,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n maxWidth: 240,\n};\n\nconst ITEM_STYLE: CSSProperties = {\n appearance: 'none',\n display: 'block',\n width: '100%',\n textAlign: 'left',\n background: 'transparent',\n border: 'none',\n borderRadius: 6,\n color: 'inherit',\n cursor: 'pointer',\n font: 'inherit',\n padding: '6px 10px',\n};\n\n/** Same derivation as the core §14 label default: `attrs.label ?? id`. */\nfunction nodeLabelText(node: GraphNode<any>): string {\n const attrs = node.attrs as Record<string, unknown> | undefined;\n const label = attrs?.['label'];\n return label === undefined || label === null ? String(node.id) : String(label);\n}\n\nfunction legacyCopy(text: string): void {\n if (typeof document === 'undefined') return;\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.setAttribute('readonly', '');\n ta.style.position = 'fixed';\n ta.style.left = '-9999px';\n document.body.appendChild(ta);\n ta.select();\n try {\n document.execCommand('copy');\n } catch {\n /* copy unavailable in this environment */\n }\n ta.remove();\n}\n\n/** navigator.clipboard.writeText with the execCommand('copy') fallback. */\nfunction copyText(text: string): void {\n const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : undefined;\n if (clipboard !== undefined && typeof clipboard.writeText === 'function') {\n void clipboard.writeText(text).catch(() => {\n legacyCopy(text);\n });\n return;\n }\n legacyCopy(text);\n}\n\n/** §16.2 path-pair state threaded into the node items when the host\n * provided `onFindPath` (null otherwise — the items don't render). */\ninterface PathPairContext {\n onFindPath: (sourceId: NodeId, targetId: NodeId) => void;\n anchorId: NodeId | null;\n setAnchor: (id: NodeId | null) => void;\n}\n\n/** Default node-target items. Pin/Unpin toggles on the store's pin slice at\n * open/render time; Expand shows a busy state while the node's expansion is\n * in flight (§9.2 pendingExpansions); Isolate hard-scopes to the node's\n * 1-hop ego network (§9.2); Collapse/Expand folds the node's neighbourhood\n * behind it (§16.3 — containment, distinct from Expand neighbors, which\n * fetches). */\nfunction defaultNodeItems(\n instance: AnyGraphInstance,\n node: GraphNode<any>,\n pendingExpansions: ReadonlySet<NodeId>,\n pathPair: PathPairContext | null,\n): GraphContextMenuItem[] {\n const pinned = instance.store.getState().pins.has(node.id);\n const expanding = pendingExpansions.has(node.id);\n const folded = instance.getFold(node.id) !== null;\n const items: GraphContextMenuItem[] = [\n {\n id: 'focus',\n label: 'Focus',\n onSelect: () => {\n instance.focusNode(node.id);\n },\n },\n {\n id: 'select-neighbors',\n label: 'Select neighbors',\n onSelect: () => {\n instance.selectNeighbors(node.id);\n },\n },\n {\n id: 'expand',\n label: expanding ? 'Expanding…' : 'Expand neighbors',\n disabled: expanding,\n onSelect: () => {\n // A discard/collapse rejects the promise by design (§9.2); the menu\n // fires-and-forgets, so swallow it here — diagnostics carry the why.\n void instance.expandNode(node.id).catch(() => {});\n },\n },\n {\n id: 'isolate',\n label: 'Isolate',\n onSelect: () => {\n instance.applyHostUpdate({ subgraph: { seedIds: [node.id], hops: 1 } });\n },\n },\n folded\n ? {\n id: 'unfold',\n label: 'Expand neighborhood',\n onSelect: () => {\n instance.unfoldNode(node.id);\n },\n }\n : {\n id: 'fold',\n label: 'Collapse neighborhood',\n onSelect: () => {\n instance.foldNode(node.id);\n },\n },\n pinned\n ? {\n id: 'unpin',\n label: 'Unpin',\n onSelect: () => {\n instance.unpinNode(node.id);\n },\n }\n : {\n id: 'pin',\n label: 'Pin',\n onSelect: () => {\n instance.pinNode(node.id);\n },\n },\n {\n id: 'hide',\n label: 'Hide',\n onSelect: () => {\n instance.hideNodes([node.id]);\n },\n },\n {\n id: 'copy-id',\n label: 'Copy id',\n onSelect: () => {\n copyText(String(node.id));\n },\n },\n ];\n if (pathPair !== null) {\n items.push({\n id: 'find-path-from',\n label: 'Find path from here',\n onSelect: () => {\n // Re-selecting on another node re-anchors (last 'from' wins).\n pathPair.setAnchor(node.id);\n },\n });\n if (pathPair.anchorId !== null && pathPair.anchorId !== node.id) {\n const anchorId = pathPair.anchorId;\n items.push({\n id: 'find-path-to',\n label: 'Find path to here',\n onSelect: () => {\n // A completed pair consumes the anchor (one findPath per pair).\n pathPair.setAnchor(null);\n pathPair.onFindPath(anchorId, node.id);\n },\n });\n }\n }\n return items;\n}\n\nfunction defaultBackgroundItems(\n instance: AnyGraphInstance,\n scope: SubgraphSpec | null,\n): GraphContextMenuItem[] {\n const items: GraphContextMenuItem[] = [\n {\n id: 'select-all',\n label: 'Select all',\n onSelect: () => {\n instance.selectAll();\n },\n },\n {\n id: 'clear-selection',\n label: 'Clear selection',\n onSelect: () => {\n instance.clearSelection();\n },\n },\n {\n id: 'fit-view',\n label: 'Fit view',\n onSelect: () => {\n instance.fitView();\n },\n },\n ];\n if (scope !== null) {\n items.push({\n id: 'reset-isolation',\n label: 'Reset isolation',\n onSelect: () => {\n instance.resetIsolation();\n },\n });\n }\n return items;\n}\n\nconst selectScope = (s: GraphStoreState): SubgraphSpec | null => s.scope;\nconst selectPendingExpansions = (s: GraphStoreState): ReadonlySet<NodeId> => s.pendingExpansions;\n\n/** Store slice subscription local to this component (the ambient hooks in\n * ../hooks require the provider; here the instance may be an explicit prop). */\nfunction useInstanceStore<T>(instance: AnyGraphInstance, selector: (s: GraphStoreState) => T): T {\n const subscribe = useCallback(\n (onChange: () => void) => instance.store.subscribe(onChange),\n [instance],\n );\n const selectorRef = useRef(selector);\n selectorRef.current = selector;\n const getSnapshot = useCallback(() => selectorRef.current(instance.store.getState()), [instance]);\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\nexport function GraphContextMenu(props: GraphContextMenuProps): ReactElement | null {\n const instance = useResolvedInstance(props.instance, '<GraphContextMenu>');\n // §9.2 state the default items reflect live while the menu is open: the\n // expand item's busy flag and the background reset-isolation gate.\n const scope = useInstanceStore(instance, selectScope);\n const pendingExpansions = useInstanceStore(instance, selectPendingExpansions);\n\n const [open, setOpen] = useState<OpenState | null>(null);\n const [activeIndex, setActiveIndex] = useState(0);\n const rootRef = useRef<HTMLDivElement | null>(null);\n const itemRefs = useRef<(HTMLElement | null)[]>([]);\n /** Element focused when the menu opened; Escape/activation restores it. */\n const restoreFocusRef = useRef<HTMLElement | null>(null);\n /** §16.2 pending 'Find path from here' anchor. A ref, not state: it only\n * changes via item activation, which closes the menu — the next open\n * re-renders and reads it fresh. Session-local, never serialized. */\n const pathAnchorRef = useRef<NodeId | null>(null);\n\n const close = useCallback((restoreFocus: boolean): void => {\n setOpen(null);\n const el = restoreFocusRef.current;\n restoreFocusRef.current = null;\n if (restoreFocus && el !== null && el.isConnected) el.focus();\n }, []);\n\n // The typed 'contextMenu' event is the only open channel.\n useEffect(() => {\n const off = instance.on('contextMenu', (payload) => {\n restoreFocusRef.current =\n typeof document !== 'undefined' && document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null;\n setOpen({ target: payload.target, screen: payload.screen });\n setActiveIndex(0);\n });\n return off;\n }, [instance]);\n\n // Outside pointerdown (capture, so it runs before anything swallows it) and\n // window blur both dismiss without stealing focus back.\n useEffect(() => {\n if (open === null) return undefined;\n const onDocPointerDown = (e: Event): void => {\n const root = rootRef.current;\n if (root !== null && e.target instanceof Node && root.contains(e.target)) return;\n close(false);\n };\n const onWindowBlur = (): void => {\n close(false);\n };\n document.addEventListener('pointerdown', onDocPointerDown, true);\n window.addEventListener('blur', onWindowBlur);\n return () => {\n document.removeEventListener('pointerdown', onDocPointerDown, true);\n window.removeEventListener('blur', onWindowBlur);\n };\n }, [open, close]);\n\n // The menu opens focused; roving focus follows activeIndex.\n useEffect(() => {\n if (open === null) return;\n const el = itemRefs.current[activeIndex] ?? itemRefs.current[0] ?? rootRef.current;\n el?.focus();\n }, [open, activeIndex]);\n\n if (open === null) return null;\n\n const onFindPath = props.onFindPath;\n const defaultItems =\n open.target.kind === 'node'\n ? defaultNodeItems(\n instance,\n open.target.node,\n pendingExpansions,\n onFindPath !== undefined\n ? {\n onFindPath,\n anchorId: pathAnchorRef.current,\n setAnchor: (id) => {\n pathAnchorRef.current = id;\n },\n }\n : null,\n )\n : defaultBackgroundItems(instance, scope);\n const items =\n props.renderItems !== undefined\n ? props.renderItems({ target: open.target, defaultItems })\n : defaultItems;\n const heading = open.target.kind === 'node' ? nodeLabelText(open.target.node) : null;\n\n const activate = (item: GraphContextMenuItem): void => {\n close(true);\n item.onSelect();\n };\n\n const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>): void => {\n const count = items.length;\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n setActiveIndex((i) => (count === 0 ? 0 : (i + 1) % count));\n break;\n case 'ArrowUp':\n e.preventDefault();\n setActiveIndex((i) => (count === 0 ? 0 : (i - 1 + count) % count));\n break;\n case 'Home':\n e.preventDefault();\n setActiveIndex(0);\n break;\n case 'End':\n e.preventDefault();\n setActiveIndex(count === 0 ? 0 : count - 1);\n break;\n case 'Enter':\n case ' ': {\n e.preventDefault();\n const item = items[activeIndex];\n if (item !== undefined && item.disabled !== true) activate(item);\n break;\n }\n case 'Escape':\n e.preventDefault();\n close(true);\n break;\n case 'Tab':\n e.preventDefault(); // focus stays inside the menu while open\n break;\n default:\n break;\n }\n };\n\n const positionStyle: CSSProperties = {\n position: 'absolute',\n left: open.screen[0],\n top: open.screen[1],\n };\n\n return (\n <div\n ref={rootRef}\n data-orbit-context-menu=\"\"\n role=\"menu\"\n aria-label={heading ?? 'Graph'}\n tabIndex={-1}\n className={props.className}\n style={\n props.className !== undefined\n ? { ...positionStyle, ...props.style }\n : { ...MENU_CHROME, ...positionStyle, ...props.style }\n }\n onKeyDown={onKeyDown}\n >\n {heading !== null ? (\n // Target-derived text renders as a TEXT NODE (§14/§15 injection ban).\n <div data-orbit-context-menu-heading=\"\" role=\"presentation\" style={HEADING_STYLE}>\n {heading}\n </div>\n ) : null}\n {items.map((item, index) => (\n <button\n key={item.id}\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"menuitem\"\n data-orbit-context-menu-item={item.id}\n tabIndex={index === activeIndex ? 0 : -1}\n disabled={item.disabled === true}\n className={props.itemClassName}\n style={props.itemClassName !== undefined ? undefined : ITEM_STYLE}\n onClick={() => {\n if (item.disabled !== true) activate(item);\n }}\n onPointerEnter={() => {\n setActiveIndex(index);\n }}\n >\n {item.label}\n </button>\n ))}\n </div>\n );\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { CSSProperties, ReactElement } from 'react';
2
+ import { A as AnyGraphInstance } from '../GraphProvider-B8ITDXS0.js';
3
+ import '@modernrelay/orbit-core';
4
+
5
+ /**
6
+ * <GraphHistogram> — §16.6 crossfilter histogram (S9).
7
+ *
8
+ * SVG dual-layer bar chart over one crossfilter dimension: total bars
9
+ * (muted) with the joint-filtered layer (accent) overlaid. Numeric/temporal
10
+ * dimensions brush by pointer-drag over the plot (any drag REPLACES the
11
+ * brush), double-click clears, ArrowLeft/ArrowRight nudge the window by one
12
+ * bin, Escape clears (the plot is focusable). Categorical dimensions render
13
+ * clickable rows toggling category exclusions. All text values (dimension
14
+ * key, category keys) render as TEXT NODES. Headless-styleable: providing
15
+ * `className` drops the built-in dark-theme inline styles.
16
+ */
17
+
18
+ interface GraphHistogramProps {
19
+ /** Crossfilter dimension key (must appear in the `crossfilter` prop). */
20
+ dimension: string;
21
+ /** Plot height in CSS px. Default 96. */
22
+ height?: number;
23
+ /** Explicit instance; defaults to the ambient <Graph>/<GraphProvider> one. */
24
+ instance?: AnyGraphInstance;
25
+ /** Class hook for the root; providing it drops the default styles. */
26
+ className?: string;
27
+ style?: CSSProperties;
28
+ /** Class hook for each categorical row; providing it drops its defaults. */
29
+ categoryClassName?: string;
30
+ }
31
+ declare function GraphHistogram(props: GraphHistogramProps): ReactElement;
32
+
33
+ export { GraphHistogram, type GraphHistogramProps };
@@ -0,0 +1,216 @@
1
+ import { useRangeBrush, brushRangePct, sliderValueText, sliderValueNow, toggleCategoryExclusion } from '../chunk-DGFLU2SN.js';
2
+ import { useCrossfilterDimension, useSessionSetBrush } from '../chunk-JN7QCMFW.js';
3
+ import { useResolvedInstance } from '../chunk-7XB47YST.js';
4
+ import { jsx, jsxs } from 'react/jsx-runtime';
5
+
6
+ var ROOT_STYLE = {
7
+ display: "flex",
8
+ flexDirection: "column",
9
+ gap: 4,
10
+ padding: 8,
11
+ borderRadius: 8,
12
+ background: "rgba(23, 25, 32, 0.92)",
13
+ border: "1px solid rgba(255, 255, 255, 0.14)",
14
+ color: "#e8eaf0",
15
+ font: "12px/1.4 system-ui, sans-serif"
16
+ };
17
+ var TITLE_STYLE = { opacity: 0.75 };
18
+ var PLOT_STYLE = {
19
+ position: "relative",
20
+ cursor: "crosshair",
21
+ touchAction: "none",
22
+ userSelect: "none"
23
+ };
24
+ var SVG_STYLE = { display: "block", width: "100%" };
25
+ var CATEGORY_ROW_STYLE = {
26
+ appearance: "none",
27
+ display: "flex",
28
+ justifyContent: "space-between",
29
+ gap: 8,
30
+ background: "transparent",
31
+ border: "none",
32
+ borderRadius: 4,
33
+ color: "inherit",
34
+ cursor: "pointer",
35
+ font: "inherit",
36
+ padding: "2px 4px",
37
+ textAlign: "left"
38
+ };
39
+ var EXCLUDED_ROW_STYLE = {
40
+ ...CATEGORY_ROW_STYLE,
41
+ opacity: 0.45,
42
+ textDecoration: "line-through"
43
+ };
44
+ var TOTAL_FILL = "rgba(148, 163, 184, 0.35)";
45
+ var FILTERED_FILL = "#7aa2ff";
46
+ var BRUSH_FILL = "rgba(122, 162, 255, 0.18)";
47
+ var BRUSH_STROKE = "rgba(122, 162, 255, 0.85)";
48
+ function GraphHistogram(props) {
49
+ const instance = useResolvedInstance(props.instance, "<GraphHistogram>");
50
+ const { summary, brush } = useCrossfilterDimension(instance, props.dimension);
51
+ const setBrush = useSessionSetBrush(instance, props.dimension);
52
+ const domain = summary?.domain ?? null;
53
+ const handlers = useRangeBrush({
54
+ domain,
55
+ binCount: summary?.bins.length ?? 0,
56
+ brush,
57
+ setBrush
58
+ });
59
+ const styled = props.className === void 0;
60
+ const height = props.height ?? 96;
61
+ const categorical = summary !== null && summary.kind === "categorical";
62
+ const range = brushRangePct(brush, domain);
63
+ const renderCategory = (cat, maxTotal) => {
64
+ const pct = maxTotal > 0 ? Math.round(cat.filtered / maxTotal * 100) : 0;
65
+ return /* @__PURE__ */ jsxs(
66
+ "button",
67
+ {
68
+ type: "button",
69
+ "data-orbit-histogram-category": cat.key,
70
+ "aria-pressed": cat.excluded,
71
+ "aria-label": `Toggle category ${cat.key}`,
72
+ className: props.categoryClassName,
73
+ style: props.categoryClassName !== void 0 ? void 0 : cat.excluded ? EXCLUDED_ROW_STYLE : CATEGORY_ROW_STYLE,
74
+ onClick: () => {
75
+ setBrush(toggleCategoryExclusion(brush, cat.key));
76
+ },
77
+ children: [
78
+ /* @__PURE__ */ jsx("span", { "data-orbit-histogram-category-label": "", children: cat.key }),
79
+ /* @__PURE__ */ jsxs(
80
+ "span",
81
+ {
82
+ "data-orbit-histogram-category-count": "",
83
+ "aria-hidden": "true",
84
+ style: props.categoryClassName !== void 0 ? void 0 : {
85
+ background: `linear-gradient(to right, ${FILTERED_FILL}33 ${pct}%, transparent ${pct}%)`,
86
+ borderRadius: 2,
87
+ padding: "0 4px"
88
+ },
89
+ children: [
90
+ cat.filtered,
91
+ "/",
92
+ cat.total
93
+ ]
94
+ }
95
+ )
96
+ ]
97
+ },
98
+ cat.key
99
+ );
100
+ };
101
+ let body;
102
+ if (summary === null) {
103
+ body = /* @__PURE__ */ jsx("div", { "data-orbit-histogram-empty": "", style: styled ? TITLE_STYLE : void 0, children: "No data" });
104
+ } else if (categorical) {
105
+ const maxTotal = summary.categories.reduce((m, c) => c.total > m ? c.total : m, 0);
106
+ body = /* @__PURE__ */ jsx(
107
+ "div",
108
+ {
109
+ "data-orbit-histogram-categories": "",
110
+ style: styled ? { display: "flex", flexDirection: "column" } : void 0,
111
+ children: summary.categories.map((cat) => renderCategory(cat, maxTotal))
112
+ }
113
+ );
114
+ } else {
115
+ const bins = summary.bins;
116
+ const maxTotal = bins.reduce((m, b) => b.total > m ? b.total : m, 0);
117
+ const barW = bins.length > 0 ? 100 / bins.length : 100;
118
+ body = /* @__PURE__ */ jsx(
119
+ "div",
120
+ {
121
+ "data-orbit-histogram-plot": "",
122
+ tabIndex: 0,
123
+ "aria-label": `${props.dimension} brush: drag to filter, arrow keys nudge, Escape clears`,
124
+ role: "slider",
125
+ "aria-orientation": "horizontal",
126
+ "aria-valuemin": summary.domain?.min ?? 0,
127
+ "aria-valuemax": summary.domain?.max ?? 0,
128
+ "aria-valuenow": sliderValueNow(brush, summary.domain),
129
+ "aria-valuetext": sliderValueText(brush, summary.domain),
130
+ style: styled ? PLOT_STYLE : void 0,
131
+ onPointerDown: handlers.onPointerDown,
132
+ onPointerMove: handlers.onPointerMove,
133
+ onPointerUp: handlers.onPointerUp,
134
+ onPointerCancel: handlers.onPointerCancel,
135
+ onDoubleClick: handlers.onDoubleClick,
136
+ onKeyDown: handlers.onKeyDown,
137
+ children: /* @__PURE__ */ jsxs(
138
+ "svg",
139
+ {
140
+ width: "100%",
141
+ height,
142
+ viewBox: "0 0 100 100",
143
+ preserveAspectRatio: "none",
144
+ "aria-hidden": "true",
145
+ focusable: "false",
146
+ style: styled ? SVG_STYLE : void 0,
147
+ children: [
148
+ bins.map((bin, i) => {
149
+ const x = i * barW + barW * 0.05;
150
+ const w = barW * 0.9;
151
+ const totalH = maxTotal > 0 ? bin.total / maxTotal * 100 : 0;
152
+ const filteredH = maxTotal > 0 ? bin.filtered / maxTotal * 100 : 0;
153
+ return /* @__PURE__ */ jsxs("g", { children: [
154
+ /* @__PURE__ */ jsx(
155
+ "rect",
156
+ {
157
+ "data-orbit-histogram-bar-total": "",
158
+ x,
159
+ y: 100 - totalH,
160
+ width: w,
161
+ height: totalH,
162
+ fill: TOTAL_FILL
163
+ }
164
+ ),
165
+ /* @__PURE__ */ jsx(
166
+ "rect",
167
+ {
168
+ "data-orbit-histogram-bar-filtered": "",
169
+ x,
170
+ y: 100 - filteredH,
171
+ width: w,
172
+ height: filteredH,
173
+ fill: FILTERED_FILL
174
+ }
175
+ )
176
+ ] }, i);
177
+ }),
178
+ range !== null ? /* @__PURE__ */ jsx(
179
+ "rect",
180
+ {
181
+ "data-orbit-histogram-brush": "",
182
+ x: range.leftPct,
183
+ y: 0,
184
+ width: range.widthPct,
185
+ height: 100,
186
+ fill: BRUSH_FILL,
187
+ stroke: BRUSH_STROKE,
188
+ strokeWidth: 0.5,
189
+ pointerEvents: "none"
190
+ }
191
+ ) : null
192
+ ]
193
+ }
194
+ )
195
+ }
196
+ );
197
+ }
198
+ return /* @__PURE__ */ jsxs(
199
+ "div",
200
+ {
201
+ "data-orbit-histogram": props.dimension,
202
+ role: "group",
203
+ "aria-label": `${props.dimension} histogram`,
204
+ className: props.className,
205
+ style: styled ? { ...ROOT_STYLE, ...props.style } : props.style,
206
+ children: [
207
+ /* @__PURE__ */ jsx("div", { "data-orbit-histogram-title": "", style: styled ? TITLE_STYLE : void 0, children: props.dimension }),
208
+ body
209
+ ]
210
+ }
211
+ );
212
+ }
213
+
214
+ export { GraphHistogram };
215
+ //# sourceMappingURL=Histogram.js.map
216
+ //# sourceMappingURL=Histogram.js.map