@ankhorage/zora 20.1.6 → 20.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.d.ts +13 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.js +71 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.d.ts +87 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js +132 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/bindGraphEvents.d.ts +7 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/bindGraphEvents.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/bindGraphEvents.js +59 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/bindGraphEvents.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts +7 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js +32 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.d.ts +22 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.js +41 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.d.ts +30 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js +222 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createRenderableGraphElements.d.ts +5 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createRenderableGraphElements.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createRenderableGraphElements.js +51 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createRenderableGraphElements.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/fitGraphViewport.d.ts +5 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/fitGraphViewport.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/fitGraphViewport.js +18 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/fitGraphViewport.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/readGraphRenderedNodes.d.ts +5 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/readGraphRenderedNodes.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/readGraphRenderedNodes.js +14 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/readGraphRenderedNodes.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/runGraphLayout.d.ts +14 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/runGraphLayout.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/runGraphLayout.js +34 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/runGraphLayout.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/scheduleGraphFrame.d.ts +3 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/scheduleGraphFrame.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/scheduleGraphFrame.js +10 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/scheduleGraphFrame.js.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/syncGraphElements.d.ts +5 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/syncGraphElements.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/syncGraphElements.js +12 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/syncGraphElements.js.map +1 -0
- package/dist/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.d.ts.map +1 -1
- package/dist/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.js +25 -10
- package/dist/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.js.map +1 -1
- package/package.json +6 -4
- package/src/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.tsx +142 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx +293 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/bindGraphEvents.ts +102 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.ts +38 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.ts +80 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.ts +301 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/createRenderableGraphElements.test.ts +49 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/createRenderableGraphElements.ts +72 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/fitGraphViewport.ts +20 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/readGraphRenderedNodes.ts +20 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/runGraphLayout.ts +53 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/scheduleGraphFrame.ts +13 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/syncGraphElements.ts +19 -0
- package/src/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.test.ts +47 -31
- package/src/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.ts +47 -11
- package/src/types/cytoscape-elk.d.ts +4 -0
- package/web-dist/graph-view/GraphView.d.ts +87 -0
- package/web-dist/graph-view/GraphView.js +123308 -0
- package/web-dist/graph-view/artifact.json +7 -0
- package/web-dist/tree-view/artifact.json +7 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materializeWebComponentArtifactAsync.js","sourceRoot":"","sources":["../../../../../src/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.ts"],"names":[],"mappings":"AAgBA,8FAA8F;AAC9F,MAAM,CAAC,KAAK,UAAU,oCAAoC,CACxD,KAA2C,EAC3C,UAA8C;IAE9C,MAAM,
|
|
1
|
+
{"version":3,"file":"materializeWebComponentArtifactAsync.js","sourceRoot":"","sources":["../../../../../src/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.ts"],"names":[],"mappings":"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","sourcesContent":["import type { WebComponentArtifactFileSystemPort } from '../ports/outbound/WebComponentArtifactFileSystemPort';\n\nexport interface MaterializeWebComponentArtifactInput {\n readonly component: string;\n readonly outputDirectory: string;\n readonly packageRoot: string;\n readonly packageVersion: string;\n}\n\nexport interface MaterializeWebComponentArtifactResult {\n readonly component: string;\n readonly createdFiles: readonly string[];\n readonly outputDirectory: string;\n readonly platform: 'web';\n}\n\n/*** Materialize one replaceable web-only component artifact from the published ZORA build. */\nexport async function materializeWebComponentArtifactAsync(\n input: MaterializeWebComponentArtifactInput,\n fileSystem: WebComponentArtifactFileSystemPort,\n): Promise<MaterializeWebComponentArtifactResult> {\n const sourceDirectory = fileSystem.joinPath(input.packageRoot, 'web-dist', input.component);\n const artifact = await readWebArtifactDefinitionAsync(\n sourceDirectory,\n input.component,\n fileSystem,\n );\n await fileSystem.ensureDirectoryAsync(input.outputDirectory);\n\n const createdFiles = await Promise.all(\n artifact.files.map(async (fileName) => {\n const sourcePath = fileSystem.joinPath(sourceDirectory, fileName);\n const targetPath = fileSystem.joinPath(input.outputDirectory, fileName);\n const source = await fileSystem.readTextFileAsync(sourcePath);\n await fileSystem.writeTextFileAsync(\n targetPath,\n createGeneratedSource(input.packageVersion, source),\n );\n return targetPath;\n }),\n );\n\n const metadataPath = fileSystem.joinPath(input.outputDirectory, 'zora-artifact.json');\n await fileSystem.writeTextFileAsync(\n metadataPath,\n `${JSON.stringify(\n {\n generated: true,\n owner: '@ankhorage/zora',\n version: input.packageVersion,\n component: input.component,\n platform: 'web',\n },\n null,\n 2,\n )}\\n`,\n );\n\n return {\n component: input.component,\n createdFiles: [...createdFiles, metadataPath],\n outputDirectory: input.outputDirectory,\n platform: 'web',\n };\n}\n\ninterface WebArtifactDefinition {\n readonly component: string;\n readonly files: readonly string[];\n}\n\n/*** Read one packaged artifact manifest so new web components require no CLI branching. */\nasync function readWebArtifactDefinitionAsync(\n sourceDirectory: string,\n component: string,\n fileSystem: WebComponentArtifactFileSystemPort,\n): Promise<WebArtifactDefinition> {\n const manifestPath = fileSystem.joinPath(sourceDirectory, 'artifact.json');\n const manifestSource = await readWebArtifactManifestSourceAsync(\n manifestPath,\n component,\n fileSystem,\n );\n const parsed = JSON.parse(manifestSource) as {\n readonly component?: unknown;\n readonly files?: unknown;\n };\n if (\n parsed.component !== component ||\n !Array.isArray(parsed.files) ||\n parsed.files.length === 0 ||\n !parsed.files.every((fileName) => typeof fileName === 'string' && fileName.length > 0)\n ) {\n throw new Error(`Invalid ZORA web artifact manifest: ${component}`);\n }\n\n return {\n component,\n files: parsed.files,\n };\n}\n\n/*** Read an artifact manifest and translate missing packages into the CLI-facing error. */\nasync function readWebArtifactManifestSourceAsync(\n manifestPath: string,\n component: string,\n fileSystem: WebComponentArtifactFileSystemPort,\n): Promise<string> {\n try {\n return await fileSystem.readTextFileAsync(manifestPath);\n } catch {\n throw new Error(`Unsupported ZORA web component: ${component}`);\n }\n}\n\n/*** Add a generated-file marker without changing the compiled artifact semantics. */\nfunction createGeneratedSource(packageVersion: string, source: string): string {\n return `// Generated by @ankhorage/zora v${packageVersion}. Do not edit manually.\\n${source}`;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "20.1
|
|
4
|
+
"version": "20.2.1",
|
|
5
5
|
"description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/zora#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@ankhorage/color-theory": "^0.3.1",
|
|
44
|
-
"@ankhorage/contracts": "^22.0
|
|
45
|
-
"@ankhorage/surface": "^9.0.
|
|
44
|
+
"@ankhorage/contracts": "^22.1.0",
|
|
45
|
+
"@ankhorage/surface": "^9.0.7",
|
|
46
46
|
"@ankhorage/utility": "^1.4.0",
|
|
47
47
|
"expo-document-picker": "~57.0.1",
|
|
48
48
|
"expo-image-picker": "~57.0.16"
|
|
@@ -142,7 +142,9 @@
|
|
|
142
142
|
"react-native-svg": "15.15.4",
|
|
143
143
|
"react-native-web": "~0.21.0",
|
|
144
144
|
"react-native-worklets": "0.10.1",
|
|
145
|
-
"typescript": "~6.0.3"
|
|
145
|
+
"typescript": "~6.0.3",
|
|
146
|
+
"cytoscape": "^3.34.3",
|
|
147
|
+
"cytoscape-elk": "^2.3.0"
|
|
146
148
|
},
|
|
147
149
|
"packageManager": "bun@1.4.2",
|
|
148
150
|
"ankh": {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
GraphViewElementEventType,
|
|
7
|
+
GraphViewNode,
|
|
8
|
+
GraphViewNodeRenderContext,
|
|
9
|
+
GraphViewRenderedNode,
|
|
10
|
+
GraphViewSize,
|
|
11
|
+
} from './GraphView';
|
|
12
|
+
|
|
13
|
+
interface GraphNodeOverlayProps {
|
|
14
|
+
readonly nodes: readonly GraphViewNode[];
|
|
15
|
+
readonly renderedNodes: readonly GraphViewRenderedNode[];
|
|
16
|
+
readonly renderNode: (context: GraphViewNodeRenderContext) => React.ReactNode;
|
|
17
|
+
readonly onNodeEvent: (id: string, type: GraphViewElementEventType) => void;
|
|
18
|
+
readonly onNodeSize: (id: string, size: GraphViewSize) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/*** Render optional React node content above Cytoscape at its authoritative rendered positions. */
|
|
22
|
+
export function GraphNodeOverlay(props: GraphNodeOverlayProps) {
|
|
23
|
+
const nodesById = React.useMemo(
|
|
24
|
+
() => new Map(props.nodes.map((node) => [node.id, node] as const)),
|
|
25
|
+
[props.nodes],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
aria-hidden
|
|
31
|
+
style={{
|
|
32
|
+
inset: 0,
|
|
33
|
+
overflow: 'hidden',
|
|
34
|
+
pointerEvents: 'none',
|
|
35
|
+
position: 'absolute',
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{props.renderedNodes.map((renderedNode) => {
|
|
39
|
+
const node = nodesById.get(renderedNode.id);
|
|
40
|
+
if (!node) return null;
|
|
41
|
+
return (
|
|
42
|
+
<MeasuredGraphNode
|
|
43
|
+
key={node.id}
|
|
44
|
+
node={node}
|
|
45
|
+
renderedNode={renderedNode}
|
|
46
|
+
renderNode={props.renderNode}
|
|
47
|
+
onNodeEvent={props.onNodeEvent}
|
|
48
|
+
onNodeSize={props.onNodeSize}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
})}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface MeasuredGraphNodeProps {
|
|
57
|
+
readonly node: GraphViewNode;
|
|
58
|
+
readonly renderedNode: GraphViewRenderedNode;
|
|
59
|
+
readonly renderNode: GraphNodeOverlayProps['renderNode'];
|
|
60
|
+
readonly onNodeEvent: GraphNodeOverlayProps['onNodeEvent'];
|
|
61
|
+
readonly onNodeSize: GraphNodeOverlayProps['onNodeSize'];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/*** Measure one rich node at model scale and translate its DOM interactions into graph events. */
|
|
65
|
+
function MeasuredGraphNode(props: MeasuredGraphNodeProps) {
|
|
66
|
+
const elementRef = React.useRef<HTMLDivElement | null>(null);
|
|
67
|
+
const { node, onNodeSize, renderedNode } = props;
|
|
68
|
+
|
|
69
|
+
React.useLayoutEffect(() => {
|
|
70
|
+
const element = elementRef.current;
|
|
71
|
+
if (element === null) return;
|
|
72
|
+
reportElementSize(element, node.id, onNodeSize);
|
|
73
|
+
|
|
74
|
+
const Observer = readResizeObserverConstructor();
|
|
75
|
+
if (Observer === null) return;
|
|
76
|
+
const observer = new Observer(() => reportElementSize(element, node.id, onNodeSize));
|
|
77
|
+
observer.observe(element);
|
|
78
|
+
return () => observer.disconnect();
|
|
79
|
+
}, [node.id, onNodeSize]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
ref={(element) => {
|
|
84
|
+
elementRef.current = element;
|
|
85
|
+
}}
|
|
86
|
+
style={{
|
|
87
|
+
left: renderedNode.position.x,
|
|
88
|
+
pointerEvents: 'auto',
|
|
89
|
+
position: 'absolute',
|
|
90
|
+
top: renderedNode.position.y,
|
|
91
|
+
transform: `translate(-50%, -50%) scale(${renderedNode.zoom})`,
|
|
92
|
+
transformOrigin: 'center',
|
|
93
|
+
}}
|
|
94
|
+
onClick={() => props.onNodeEvent(node.id, 'press')}
|
|
95
|
+
onDoubleClick={() => props.onNodeEvent(node.id, 'double-press')}
|
|
96
|
+
onMouseEnter={() => props.onNodeEvent(node.id, 'pointer-enter')}
|
|
97
|
+
onMouseLeave={() => props.onNodeEvent(node.id, 'pointer-leave')}
|
|
98
|
+
>
|
|
99
|
+
{props.renderNode({
|
|
100
|
+
hovered: renderedNode.hovered,
|
|
101
|
+
node,
|
|
102
|
+
selected: renderedNode.selected,
|
|
103
|
+
zoom: renderedNode.zoom,
|
|
104
|
+
})}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/*** Report unscaled DOM dimensions only when a measurable node has positive size. */
|
|
110
|
+
function reportElementSize(
|
|
111
|
+
element: unknown,
|
|
112
|
+
nodeId: string,
|
|
113
|
+
onNodeSize: (id: string, size: GraphViewSize) => void,
|
|
114
|
+
) {
|
|
115
|
+
const measured = element as {
|
|
116
|
+
readonly offsetHeight?: unknown;
|
|
117
|
+
readonly offsetWidth?: unknown;
|
|
118
|
+
};
|
|
119
|
+
if (typeof measured.offsetWidth !== 'number' || typeof measured.offsetHeight !== 'number') return;
|
|
120
|
+
if (measured.offsetWidth <= 0 || measured.offsetHeight <= 0) return;
|
|
121
|
+
onNodeSize(nodeId, {
|
|
122
|
+
width: measured.offsetWidth,
|
|
123
|
+
height: measured.offsetHeight,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/*** Read ResizeObserver without requiring the DOM lib in the package TypeScript target. */
|
|
128
|
+
function readResizeObserverConstructor(): ResizeObserverConstructorLike | null {
|
|
129
|
+
const observer = (
|
|
130
|
+
globalThis as unknown as {
|
|
131
|
+
ResizeObserver?: ResizeObserverConstructorLike;
|
|
132
|
+
}
|
|
133
|
+
).ResizeObserver;
|
|
134
|
+
return observer ?? null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface ResizeObserverLike {
|
|
138
|
+
disconnect(): void;
|
|
139
|
+
observe(target: unknown): void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type ResizeObserverConstructorLike = new (callback: () => void) => ResizeObserverLike;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { CytoscapeOptions } from 'cytoscape';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { createGraphRuntime, type GraphRuntime } from './createGraphRuntime';
|
|
7
|
+
import { GraphNodeOverlay } from './GraphNodeOverlay';
|
|
8
|
+
|
|
9
|
+
export type GraphViewLayoutName = 'breadthfirst' | 'circle' | 'concentric' | 'elk' | 'grid';
|
|
10
|
+
export type GraphViewElementEventType =
|
|
11
|
+
'press' | 'double-press' | 'select' | 'unselect' | 'pointer-enter' | 'pointer-leave';
|
|
12
|
+
|
|
13
|
+
export interface GraphViewNode {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly label?: string;
|
|
16
|
+
readonly parentId?: string;
|
|
17
|
+
readonly classes?: string;
|
|
18
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GraphViewEdge {
|
|
22
|
+
readonly id?: string;
|
|
23
|
+
readonly source: string;
|
|
24
|
+
readonly target: string;
|
|
25
|
+
readonly classes?: string;
|
|
26
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GraphViewStyleRule {
|
|
30
|
+
readonly selector: string;
|
|
31
|
+
readonly style: Readonly<Record<string, unknown>>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GraphViewPoint {
|
|
35
|
+
readonly x: number;
|
|
36
|
+
readonly y: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface GraphViewSize {
|
|
40
|
+
readonly width: number;
|
|
41
|
+
readonly height: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GraphViewNodeRenderContext {
|
|
45
|
+
readonly hovered: boolean;
|
|
46
|
+
readonly node: GraphViewNode;
|
|
47
|
+
readonly selected: boolean;
|
|
48
|
+
readonly zoom: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GraphViewRenderedNode {
|
|
52
|
+
readonly hovered: boolean;
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly position: GraphViewPoint;
|
|
55
|
+
readonly selected: boolean;
|
|
56
|
+
readonly zoom: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface GraphViewViewport {
|
|
60
|
+
readonly zoom: number;
|
|
61
|
+
readonly pan: GraphViewPoint;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GraphViewFitOptions {
|
|
65
|
+
readonly nodeIds?: readonly string[];
|
|
66
|
+
readonly padding?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface GraphViewElementEvent {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly type: GraphViewElementEventType;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface GraphViewController {
|
|
75
|
+
fit(options?: GraphViewFitOptions): void;
|
|
76
|
+
getViewport(): GraphViewViewport;
|
|
77
|
+
setPan(pan: GraphViewPoint): void;
|
|
78
|
+
setZoom(zoom: number): void;
|
|
79
|
+
zoomBy(factor: number): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GraphViewProps {
|
|
83
|
+
readonly nodes: readonly GraphViewNode[];
|
|
84
|
+
readonly edges: readonly GraphViewEdge[];
|
|
85
|
+
readonly layout?: GraphViewLayoutName;
|
|
86
|
+
readonly layoutOptions?: Readonly<Record<string, unknown>>;
|
|
87
|
+
readonly spacingFactor?: number;
|
|
88
|
+
readonly styleRules?: readonly GraphViewStyleRule[];
|
|
89
|
+
readonly selectedNodeIds?: readonly string[];
|
|
90
|
+
readonly renderNode?: (context: GraphViewNodeRenderContext) => React.ReactNode;
|
|
91
|
+
readonly getNodeSize?: (node: GraphViewNode) => GraphViewSize | undefined;
|
|
92
|
+
readonly fitPadding?: number;
|
|
93
|
+
readonly minZoom?: number;
|
|
94
|
+
readonly maxZoom?: number;
|
|
95
|
+
readonly className?: string;
|
|
96
|
+
readonly style?: React.CSSProperties;
|
|
97
|
+
readonly ariaLabel?: string;
|
|
98
|
+
readonly onReady?: (controller: GraphViewController) => void;
|
|
99
|
+
readonly onLayoutComplete?: (controller: GraphViewController) => void;
|
|
100
|
+
readonly onNodeEvent?: (event: GraphViewElementEvent) => void;
|
|
101
|
+
readonly onEdgeEvent?: (event: GraphViewElementEvent) => void;
|
|
102
|
+
readonly onViewportChange?: (viewport: GraphViewViewport) => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type GraphViewCallbacks = Pick<
|
|
106
|
+
GraphViewProps,
|
|
107
|
+
'onEdgeEvent' | 'onLayoutComplete' | 'onNodeEvent' | 'onReady' | 'onViewportChange'
|
|
108
|
+
>;
|
|
109
|
+
|
|
110
|
+
type GraphContainer = NonNullable<CytoscapeOptions['container']>;
|
|
111
|
+
|
|
112
|
+
/*** Render a browser graph while one runtime owns Cytoscape layout, viewport, and disposal. */
|
|
113
|
+
export function GraphView(props: GraphViewProps) {
|
|
114
|
+
const { containerRef, renderedNodes, runtimeRef } = useGraphViewRuntime(props);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
aria-label={props.ariaLabel ?? 'Graph'}
|
|
119
|
+
className={props.className}
|
|
120
|
+
role="application"
|
|
121
|
+
style={{
|
|
122
|
+
height: '100%',
|
|
123
|
+
minHeight: 0,
|
|
124
|
+
minWidth: 0,
|
|
125
|
+
position: 'relative',
|
|
126
|
+
width: '100%',
|
|
127
|
+
...props.style,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<div ref={containerRef} style={{ height: '100%', width: '100%' }} />
|
|
131
|
+
{props.renderNode ? (
|
|
132
|
+
<GraphNodeOverlay
|
|
133
|
+
nodes={props.nodes}
|
|
134
|
+
renderedNodes={renderedNodes}
|
|
135
|
+
renderNode={props.renderNode}
|
|
136
|
+
onNodeEvent={(id, type) => runtimeRef.current?.handleOverlayNodeEvent(id, type)}
|
|
137
|
+
onNodeSize={(id, size) => runtimeRef.current?.setNodeSize(id, size)}
|
|
138
|
+
/>
|
|
139
|
+
) : null}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/*** Compose React synchronization around the single imperative graph runtime. */
|
|
145
|
+
function useGraphViewRuntime(props: GraphViewProps) {
|
|
146
|
+
const containerRef = React.useRef<GraphContainer | null>(null);
|
|
147
|
+
const runtimeRef = React.useRef<GraphRuntime | null>(null);
|
|
148
|
+
const callbacksRef = useGraphCallbacksRef(props);
|
|
149
|
+
const [renderedNodes, setRenderedNodes] = React.useState<readonly GraphViewRenderedNode[]>([]);
|
|
150
|
+
|
|
151
|
+
useRuntimeLifecycle(containerRef, runtimeRef, callbacksRef);
|
|
152
|
+
useRuntimeGraphUpdate(runtimeRef, props);
|
|
153
|
+
useRuntimeSelection(runtimeRef, props.selectedNodeIds);
|
|
154
|
+
useRenderedNodeSubscription(runtimeRef, props.renderNode, setRenderedNodes);
|
|
155
|
+
useKnownNodeSizes(runtimeRef, props.nodes, props.getNodeSize);
|
|
156
|
+
|
|
157
|
+
return { containerRef, renderedNodes, runtimeRef };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/*** Keep the callback ref current without mutating refs during render. */
|
|
161
|
+
function useGraphCallbacksRef(props: GraphViewProps) {
|
|
162
|
+
const callbacksRef = React.useRef<GraphViewCallbacks>(readCallbacks(props));
|
|
163
|
+
const { onEdgeEvent, onLayoutComplete, onNodeEvent, onReady, onViewportChange } = props;
|
|
164
|
+
|
|
165
|
+
React.useEffect(() => {
|
|
166
|
+
callbacksRef.current = {
|
|
167
|
+
onEdgeEvent,
|
|
168
|
+
onLayoutComplete,
|
|
169
|
+
onNodeEvent,
|
|
170
|
+
onReady,
|
|
171
|
+
onViewportChange,
|
|
172
|
+
};
|
|
173
|
+
}, [onEdgeEvent, onLayoutComplete, onNodeEvent, onReady, onViewportChange]);
|
|
174
|
+
|
|
175
|
+
return callbacksRef;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/*** Create and dispose the runtime only with the underlying browser container lifetime. */
|
|
179
|
+
function useRuntimeLifecycle(
|
|
180
|
+
containerRef: { current: GraphContainer | null },
|
|
181
|
+
runtimeRef: { current: GraphRuntime | null },
|
|
182
|
+
callbacksRef: { current: GraphViewCallbacks },
|
|
183
|
+
) {
|
|
184
|
+
React.useEffect(() => {
|
|
185
|
+
const container = containerRef.current;
|
|
186
|
+
if (container === null) return;
|
|
187
|
+
const runtime = createGraphRuntime(container, callbacksRef);
|
|
188
|
+
runtimeRef.current = runtime;
|
|
189
|
+
return () => {
|
|
190
|
+
runtime.destroy();
|
|
191
|
+
runtimeRef.current = null;
|
|
192
|
+
};
|
|
193
|
+
}, [callbacksRef, containerRef, runtimeRef]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/*** Synchronize graph data and layout policy without exposing Cytoscape to React. */
|
|
197
|
+
function useRuntimeGraphUpdate(
|
|
198
|
+
runtimeRef: { current: GraphRuntime | null },
|
|
199
|
+
props: GraphViewProps,
|
|
200
|
+
) {
|
|
201
|
+
const {
|
|
202
|
+
edges,
|
|
203
|
+
fitPadding,
|
|
204
|
+
layout,
|
|
205
|
+
layoutOptions,
|
|
206
|
+
maxZoom,
|
|
207
|
+
minZoom,
|
|
208
|
+
nodes,
|
|
209
|
+
renderNode,
|
|
210
|
+
spacingFactor,
|
|
211
|
+
styleRules,
|
|
212
|
+
} = props;
|
|
213
|
+
|
|
214
|
+
React.useEffect(() => {
|
|
215
|
+
runtimeRef.current?.update({
|
|
216
|
+
edges,
|
|
217
|
+
fitPadding,
|
|
218
|
+
layout,
|
|
219
|
+
layoutOptions,
|
|
220
|
+
maxZoom,
|
|
221
|
+
minZoom,
|
|
222
|
+
nodes,
|
|
223
|
+
richNodeRendering: renderNode !== undefined,
|
|
224
|
+
spacingFactor,
|
|
225
|
+
styleRules,
|
|
226
|
+
});
|
|
227
|
+
}, [
|
|
228
|
+
edges,
|
|
229
|
+
fitPadding,
|
|
230
|
+
layout,
|
|
231
|
+
layoutOptions,
|
|
232
|
+
maxZoom,
|
|
233
|
+
minZoom,
|
|
234
|
+
nodes,
|
|
235
|
+
renderNode,
|
|
236
|
+
runtimeRef,
|
|
237
|
+
spacingFactor,
|
|
238
|
+
styleRules,
|
|
239
|
+
]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/*** Synchronize optional controlled selection independently from graph layout. */
|
|
243
|
+
function useRuntimeSelection(
|
|
244
|
+
runtimeRef: { current: GraphRuntime | null },
|
|
245
|
+
selectedNodeIds: readonly string[] | undefined,
|
|
246
|
+
) {
|
|
247
|
+
React.useEffect(() => {
|
|
248
|
+
runtimeRef.current?.setSelectedNodeIds(selectedNodeIds);
|
|
249
|
+
}, [runtimeRef, selectedNodeIds]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/*** Subscribe the optional rich-node overlay to rendered runtime positions. */
|
|
253
|
+
function useRenderedNodeSubscription(
|
|
254
|
+
runtimeRef: { current: GraphRuntime | null },
|
|
255
|
+
renderNode: GraphViewProps['renderNode'],
|
|
256
|
+
setRenderedNodes: (nodes: readonly GraphViewRenderedNode[]) => void,
|
|
257
|
+
) {
|
|
258
|
+
React.useEffect(() => {
|
|
259
|
+
const runtime = runtimeRef.current;
|
|
260
|
+
if (runtime === null || renderNode === undefined) {
|
|
261
|
+
setRenderedNodes([]);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
return runtime.subscribeRenderedNodes(setRenderedNodes);
|
|
265
|
+
}, [renderNode, runtimeRef, setRenderedNodes]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/*** Seed known rich-node dimensions without requiring DOM measurement. */
|
|
269
|
+
function useKnownNodeSizes(
|
|
270
|
+
runtimeRef: { current: GraphRuntime | null },
|
|
271
|
+
nodes: readonly GraphViewNode[],
|
|
272
|
+
getNodeSize: GraphViewProps['getNodeSize'],
|
|
273
|
+
) {
|
|
274
|
+
React.useEffect(() => {
|
|
275
|
+
const runtime = runtimeRef.current;
|
|
276
|
+
if (runtime === null || getNodeSize === undefined) return;
|
|
277
|
+
for (const node of nodes) {
|
|
278
|
+
const size = getNodeSize(node);
|
|
279
|
+
if (size) runtime.setNodeSize(node.id, size);
|
|
280
|
+
}
|
|
281
|
+
}, [getNodeSize, nodes, runtimeRef]);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/*** Read callback props for the initial runtime callback-ref value. */
|
|
285
|
+
function readCallbacks(props: GraphViewProps): GraphViewCallbacks {
|
|
286
|
+
return {
|
|
287
|
+
onEdgeEvent: props.onEdgeEvent,
|
|
288
|
+
onLayoutComplete: props.onLayoutComplete,
|
|
289
|
+
onNodeEvent: props.onNodeEvent,
|
|
290
|
+
onReady: props.onReady,
|
|
291
|
+
onViewportChange: props.onViewportChange,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Core, EventObject } from 'cytoscape';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
GraphViewCallbacks,
|
|
5
|
+
GraphViewController,
|
|
6
|
+
GraphViewElementEventType,
|
|
7
|
+
} from './GraphView';
|
|
8
|
+
|
|
9
|
+
/*** Bind stable Cytoscape events that always read the latest React callbacks. */
|
|
10
|
+
export function bindGraphEvents(
|
|
11
|
+
cy: Core,
|
|
12
|
+
callbacksRef: { current: GraphViewCallbacks },
|
|
13
|
+
controller: GraphViewController,
|
|
14
|
+
onRuntimeChange: () => void,
|
|
15
|
+
) {
|
|
16
|
+
const nodeHandlers = bindElementEvents(cy, 'node', callbacksRef, onRuntimeChange);
|
|
17
|
+
const edgeHandlers = bindElementEvents(cy, 'edge', callbacksRef, onRuntimeChange);
|
|
18
|
+
const viewportHandler = () => {
|
|
19
|
+
callbacksRef.current.onViewportChange?.(controller.getViewport());
|
|
20
|
+
onRuntimeChange();
|
|
21
|
+
};
|
|
22
|
+
cy.on('zoom pan', viewportHandler);
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
unbindElementEvents(cy, 'node', nodeHandlers);
|
|
26
|
+
unbindElementEvents(cy, 'edge', edgeHandlers);
|
|
27
|
+
cy.off('zoom pan', viewportHandler);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ElementSelector = 'edge' | 'node';
|
|
32
|
+
|
|
33
|
+
interface BoundElementHandler {
|
|
34
|
+
readonly cytoscapeEvent: string;
|
|
35
|
+
readonly handler: (event: EventObject) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/*** Bind one selector's translated event set without resubscribing on callback changes. */
|
|
39
|
+
function bindElementEvents(
|
|
40
|
+
cy: Core,
|
|
41
|
+
selector: ElementSelector,
|
|
42
|
+
callbacksRef: { current: GraphViewCallbacks },
|
|
43
|
+
onRuntimeChange: () => void,
|
|
44
|
+
): readonly BoundElementHandler[] {
|
|
45
|
+
return EVENT_TYPES.map(([cytoscapeEvent, type]) => {
|
|
46
|
+
const handler = (event: EventObject) => {
|
|
47
|
+
const target: unknown = event.target;
|
|
48
|
+
if (!isGraphEventTarget(target)) return;
|
|
49
|
+
emitElementEvent(callbacksRef.current, selector, {
|
|
50
|
+
id: target.id(),
|
|
51
|
+
type,
|
|
52
|
+
});
|
|
53
|
+
onRuntimeChange();
|
|
54
|
+
};
|
|
55
|
+
cy.on(cytoscapeEvent, selector, handler);
|
|
56
|
+
return { cytoscapeEvent, handler };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*** Return whether a Cytoscape event target exposes the element identifier contract. */
|
|
61
|
+
function isGraphEventTarget(value: unknown): value is GraphEventTarget {
|
|
62
|
+
return (
|
|
63
|
+
typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'function'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface GraphEventTarget {
|
|
68
|
+
id(): string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/*** Emit one translated element event without dynamic callback-property access. */
|
|
72
|
+
function emitElementEvent(
|
|
73
|
+
callbacks: GraphViewCallbacks,
|
|
74
|
+
selector: ElementSelector,
|
|
75
|
+
event: { readonly id: string; readonly type: GraphViewElementEventType },
|
|
76
|
+
) {
|
|
77
|
+
if (selector === 'node') {
|
|
78
|
+
callbacks.onNodeEvent?.(event);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
callbacks.onEdgeEvent?.(event);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/*** Remove one selector's event handlers using the exact bound callback identities. */
|
|
85
|
+
function unbindElementEvents(
|
|
86
|
+
cy: Core,
|
|
87
|
+
selector: ElementSelector,
|
|
88
|
+
handlers: readonly BoundElementHandler[],
|
|
89
|
+
) {
|
|
90
|
+
for (const { cytoscapeEvent, handler } of handlers) {
|
|
91
|
+
cy.off(cytoscapeEvent, selector, handler);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const EVENT_TYPES = [
|
|
96
|
+
['tap', 'press'],
|
|
97
|
+
['dbltap', 'double-press'],
|
|
98
|
+
['select', 'select'],
|
|
99
|
+
['unselect', 'unselect'],
|
|
100
|
+
['mouseover', 'pointer-enter'],
|
|
101
|
+
['mouseout', 'pointer-leave'],
|
|
102
|
+
] as const satisfies readonly (readonly [string, GraphViewElementEventType])[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Core } from 'cytoscape';
|
|
2
|
+
|
|
3
|
+
import { fitGraphViewport } from './fitGraphViewport';
|
|
4
|
+
import type { GraphViewController } from './GraphView';
|
|
5
|
+
|
|
6
|
+
/*** Create the public viewport controller while keeping the Cytoscape core private. */
|
|
7
|
+
export function createGraphController(
|
|
8
|
+
cy: Core,
|
|
9
|
+
fitPaddingRef: { current: number },
|
|
10
|
+
): GraphViewController {
|
|
11
|
+
return {
|
|
12
|
+
fit(options) {
|
|
13
|
+
fitGraphViewport(cy, {
|
|
14
|
+
nodeIds: options?.nodeIds,
|
|
15
|
+
padding: options?.padding ?? fitPaddingRef.current,
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
getViewport() {
|
|
19
|
+
return { pan: cy.pan(), zoom: cy.zoom() };
|
|
20
|
+
},
|
|
21
|
+
setPan(pan) {
|
|
22
|
+
cy.pan(pan);
|
|
23
|
+
},
|
|
24
|
+
setZoom(zoom) {
|
|
25
|
+
cy.zoom({
|
|
26
|
+
level: zoom,
|
|
27
|
+
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 },
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
zoomBy(factor) {
|
|
31
|
+
const zoom = cy.zoom() * factor;
|
|
32
|
+
cy.zoom({
|
|
33
|
+
level: zoom,
|
|
34
|
+
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 },
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|