@graph-artifact/core 0.1.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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/ThemeContext.d.ts +47 -0
- package/dist/ThemeContext.js +81 -0
- package/dist/components/DiagramCanvas.d.ts +8 -0
- package/dist/components/DiagramCanvas.js +19 -0
- package/dist/components/GraphCanvas.d.ts +9 -0
- package/dist/components/GraphCanvas.js +104 -0
- package/dist/components/NodeDetail.d.ts +9 -0
- package/dist/components/NodeDetail.js +127 -0
- package/dist/components/edges/RoutedEdge.d.ts +11 -0
- package/dist/components/edges/RoutedEdge.js +199 -0
- package/dist/components/nodes/ClassNode.d.ts +5 -0
- package/dist/components/nodes/ClassNode.js +62 -0
- package/dist/components/nodes/EntityNode.d.ts +5 -0
- package/dist/components/nodes/EntityNode.js +57 -0
- package/dist/components/nodes/FlowNode.d.ts +5 -0
- package/dist/components/nodes/FlowNode.js +144 -0
- package/dist/components/nodes/SequenceNodes.d.ts +33 -0
- package/dist/components/nodes/SequenceNodes.js +205 -0
- package/dist/components/nodes/StateNode.d.ts +5 -0
- package/dist/components/nodes/StateNode.js +71 -0
- package/dist/components/nodes/SubgraphNode.d.ts +5 -0
- package/dist/components/nodes/SubgraphNode.js +16 -0
- package/dist/config.d.ts +138 -0
- package/dist/config.js +165 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.js +7 -0
- package/dist/diagrams/detect.d.ts +2 -0
- package/dist/diagrams/detect.js +8 -0
- package/dist/diagrams/plugins.d.ts +2 -0
- package/dist/diagrams/plugins.js +45 -0
- package/dist/diagrams/registry.d.ts +3 -0
- package/dist/diagrams/registry.js +13 -0
- package/dist/diagrams/types.d.ts +7 -0
- package/dist/diagrams/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/layout/dagre/index.d.ts +31 -0
- package/dist/layout/dagre/index.js +224 -0
- package/dist/layout/dagre/nodeSizing.d.ts +32 -0
- package/dist/layout/dagre/nodeSizing.js +202 -0
- package/dist/layout/edges/buildEdges.d.ts +18 -0
- package/dist/layout/edges/buildEdges.js +405 -0
- package/dist/layout/edges/classify.d.ts +13 -0
- package/dist/layout/edges/classify.js +36 -0
- package/dist/layout/edges/diamondHandles.d.ts +23 -0
- package/dist/layout/edges/diamondHandles.js +108 -0
- package/dist/layout/edges/index.d.ts +10 -0
- package/dist/layout/edges/index.js +8 -0
- package/dist/layout/edges/paths.d.ts +57 -0
- package/dist/layout/edges/paths.js +279 -0
- package/dist/layout/index.d.ts +59 -0
- package/dist/layout/index.js +131 -0
- package/dist/layout/intersect/circle.d.ts +2 -0
- package/dist/layout/intersect/circle.js +14 -0
- package/dist/layout/intersect/diamond.d.ts +9 -0
- package/dist/layout/intersect/diamond.js +21 -0
- package/dist/layout/intersect/index.d.ts +17 -0
- package/dist/layout/intersect/index.js +28 -0
- package/dist/layout/intersect/rect.d.ts +10 -0
- package/dist/layout/intersect/rect.js +31 -0
- package/dist/layout/intersect/rectRounded.d.ts +20 -0
- package/dist/layout/intersect/rectRounded.js +48 -0
- package/dist/layout/mindmapLayout.d.ts +13 -0
- package/dist/layout/mindmapLayout.js +299 -0
- package/dist/layout/sequenceLayout.d.ts +24 -0
- package/dist/layout/sequenceLayout.js +414 -0
- package/dist/layout/subgraph.d.ts +26 -0
- package/dist/layout/subgraph.js +63 -0
- package/dist/layout/types.d.ts +34 -0
- package/dist/layout/types.js +8 -0
- package/dist/parsers/classDiagram.d.ts +2 -0
- package/dist/parsers/classDiagram.js +105 -0
- package/dist/parsers/er.d.ts +2 -0
- package/dist/parsers/er.js +97 -0
- package/dist/parsers/flowchart.d.ts +2 -0
- package/dist/parsers/flowchart.js +191 -0
- package/dist/parsers/helpers.d.ts +4 -0
- package/dist/parsers/helpers.js +8 -0
- package/dist/parsers/index.d.ts +7 -0
- package/dist/parsers/index.js +19 -0
- package/dist/parsers/mindmap.d.ts +2 -0
- package/dist/parsers/mindmap.js +124 -0
- package/dist/parsers/sequence.d.ts +18 -0
- package/dist/parsers/sequence.js +196 -0
- package/dist/parsers/state.d.ts +2 -0
- package/dist/parsers/state.js +68 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +9 -0
- package/dist/reactDefaults.d.ts +5 -0
- package/dist/reactDefaults.js +37 -0
- package/dist/renderMarkdown.d.ts +9 -0
- package/dist/renderMarkdown.js +103 -0
- package/dist/swagger.d.ts +113 -0
- package/dist/swagger.js +551 -0
- package/dist/theme/dark.d.ts +8 -0
- package/dist/theme/dark.js +190 -0
- package/dist/theme/index.d.ts +18 -0
- package/dist/theme/index.js +29 -0
- package/dist/theme/light.d.ts +8 -0
- package/dist/theme/light.js +190 -0
- package/dist/theme/types.d.ts +97 -0
- package/dist/theme/types.js +7 -0
- package/dist/types.d.ts +235 -0
- package/dist/types.js +1 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Graph Artifact
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @graph-artifact/core
|
|
2
|
+
|
|
3
|
+
Composable diagram toolkit with:
|
|
4
|
+
- Mermaid-like parsing (`parseMermaid`)
|
|
5
|
+
- graph layout/routing engine (`layoutParsedGraph`)
|
|
6
|
+
- React renderer (`DiagramCanvas`, `GraphCanvas`)
|
|
7
|
+
- Swagger/OpenAPI focused diagram generation
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @graph-artifact/core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Peer dependencies:
|
|
16
|
+
- `react`
|
|
17
|
+
- `react-dom`
|
|
18
|
+
- `@xyflow/react`
|
|
19
|
+
- `react-markdown`
|
|
20
|
+
|
|
21
|
+
## Entry points
|
|
22
|
+
|
|
23
|
+
- `@graph-artifact/core`: headless/core APIs (Node-safe)
|
|
24
|
+
- `@graph-artifact/core/core`: explicit headless/core APIs
|
|
25
|
+
- `@graph-artifact/core/react`: React rendering APIs
|
|
26
|
+
|
|
27
|
+
## Quick start (core)
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { parseMermaid, layoutParsedGraph, getThemeTokens } from '@graph-artifact/core';
|
|
31
|
+
|
|
32
|
+
const parsed = parseMermaid('flowchart TB\nA-->B');
|
|
33
|
+
if (parsed && parsed.kind === 'graph') {
|
|
34
|
+
const theme = { ...getThemeTokens('dark'), name: 'dark' };
|
|
35
|
+
const { nodes, edges } = layoutParsedGraph(parsed, { theme });
|
|
36
|
+
console.log(nodes.length, edges.length);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start (React)
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { parseMermaid } from '@graph-artifact/core';
|
|
44
|
+
import { ThemeProvider, DiagramCanvas } from '@graph-artifact/core/react';
|
|
45
|
+
|
|
46
|
+
const parsed = parseMermaid('flowchart TB\nA-->B');
|
|
47
|
+
|
|
48
|
+
export function App() {
|
|
49
|
+
if (!parsed || parsed.kind !== 'graph') return null;
|
|
50
|
+
return (
|
|
51
|
+
<ThemeProvider>
|
|
52
|
+
<DiagramCanvas parsed={parsed} metadata={{}} onNodeClick={() => {}} />
|
|
53
|
+
</ThemeProvider>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Notes
|
|
59
|
+
|
|
60
|
+
- Use `@graph-artifact/core/react` for UI components.
|
|
61
|
+
- Use `@graph-artifact/core` for parser/layout/server workflows.
|
|
62
|
+
- For invalid syntax/layout, surface parse/layout errors to callers instead of crashing UI runtimes.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ThemeOverride } from './types.js';
|
|
3
|
+
import type { ThemeTokens } from './theme/index.js';
|
|
4
|
+
export interface Theme {
|
|
5
|
+
color: Record<string, string>;
|
|
6
|
+
font: {
|
|
7
|
+
family: string;
|
|
8
|
+
mono: string;
|
|
9
|
+
size: Record<string, string>;
|
|
10
|
+
weight: Record<string, number>;
|
|
11
|
+
};
|
|
12
|
+
space: Record<string | number, string>;
|
|
13
|
+
radius: Record<string, string>;
|
|
14
|
+
shadow: Record<string, string>;
|
|
15
|
+
lineHeight: Record<string, string>;
|
|
16
|
+
borderWidth: Record<string, string>;
|
|
17
|
+
statusColors: Record<string, {
|
|
18
|
+
bg: string;
|
|
19
|
+
text: string;
|
|
20
|
+
}>;
|
|
21
|
+
handleStyle: {
|
|
22
|
+
background: string;
|
|
23
|
+
border: string;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
};
|
|
27
|
+
edgeDefaults: {
|
|
28
|
+
style: Record<string, string | number>;
|
|
29
|
+
labelStyle: Record<string, string | number>;
|
|
30
|
+
labelBgStyle: Record<string, string | number>;
|
|
31
|
+
labelBgPadding: [number, number];
|
|
32
|
+
labelBgBorderRadius: number;
|
|
33
|
+
dashedPattern: string;
|
|
34
|
+
thickWidth: number;
|
|
35
|
+
markerSize: number;
|
|
36
|
+
};
|
|
37
|
+
nodeBase: ThemeTokens['nodeBase'];
|
|
38
|
+
nodeStyles: ThemeTokens['nodeStyles'];
|
|
39
|
+
}
|
|
40
|
+
export declare const defaultTheme: Theme;
|
|
41
|
+
interface ThemeProviderProps {
|
|
42
|
+
overrides?: ThemeOverride;
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
export declare function ThemeProvider({ overrides, children }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
46
|
+
export declare function useTheme(): Theme;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
3
|
+
import { getConfig } from './config.js';
|
|
4
|
+
import { getThemeTokens } from './theme/index.js';
|
|
5
|
+
// ─── Build Theme from Tokens ──────────────────────────────────────────────────
|
|
6
|
+
function buildTheme(tokens) {
|
|
7
|
+
return {
|
|
8
|
+
color: { ...tokens.color },
|
|
9
|
+
font: {
|
|
10
|
+
family: tokens.font.family,
|
|
11
|
+
mono: tokens.font.mono,
|
|
12
|
+
size: { ...tokens.font.size },
|
|
13
|
+
weight: { ...tokens.font.weight },
|
|
14
|
+
},
|
|
15
|
+
space: { ...tokens.space },
|
|
16
|
+
radius: { ...tokens.radius },
|
|
17
|
+
shadow: { ...tokens.shadow },
|
|
18
|
+
statusColors: { ...tokens.statusColors },
|
|
19
|
+
handleStyle: { ...tokens.handleStyle },
|
|
20
|
+
edgeDefaults: {
|
|
21
|
+
style: { ...tokens.edgeDefaults.style },
|
|
22
|
+
labelStyle: { ...tokens.edgeDefaults.labelStyle },
|
|
23
|
+
labelBgStyle: { ...tokens.edgeDefaults.labelBgStyle },
|
|
24
|
+
labelBgPadding: [...tokens.edgeDefaults.labelBgPadding],
|
|
25
|
+
labelBgBorderRadius: tokens.edgeDefaults.labelBgBorderRadius,
|
|
26
|
+
dashedPattern: tokens.edgeDefaults.dashedPattern,
|
|
27
|
+
thickWidth: tokens.edgeDefaults.thickWidth,
|
|
28
|
+
markerSize: tokens.edgeDefaults.markerSize,
|
|
29
|
+
},
|
|
30
|
+
nodeBase: tokens.nodeBase,
|
|
31
|
+
nodeStyles: tokens.nodeStyles,
|
|
32
|
+
lineHeight: { ...tokens.lineHeight },
|
|
33
|
+
borderWidth: { ...tokens.borderWidth },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Static default for context initial value (dark theme)
|
|
37
|
+
export const defaultTheme = buildTheme(getThemeTokens('dark'));
|
|
38
|
+
// ─── Deep Merge ───────────────────────────────────────────────────────────────
|
|
39
|
+
function isPlainObject(value) {
|
|
40
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && typeof value !== 'function';
|
|
41
|
+
}
|
|
42
|
+
function deepMerge(base, overrides) {
|
|
43
|
+
const result = { ...base };
|
|
44
|
+
for (const key of Object.keys(overrides)) {
|
|
45
|
+
const baseVal = base[key];
|
|
46
|
+
const overVal = overrides[key];
|
|
47
|
+
if (isPlainObject(baseVal) && isPlainObject(overVal)) {
|
|
48
|
+
result[key] = deepMerge(baseVal, overVal);
|
|
49
|
+
}
|
|
50
|
+
else if (overVal !== undefined) {
|
|
51
|
+
result[key] = overVal;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
// ─── Theme Resolution ────────────────────────────────────────────────────────
|
|
57
|
+
// Merges: base theme tokens → config.theme overrides → instance overrides
|
|
58
|
+
function resolveTheme(instanceOverrides) {
|
|
59
|
+
const { baseTheme, theme: configOverrides } = getConfig();
|
|
60
|
+
// Start from the selected base theme
|
|
61
|
+
const tokens = getThemeTokens(baseTheme);
|
|
62
|
+
let resolved = buildTheme(tokens);
|
|
63
|
+
// Layer 1: package-level config overrides
|
|
64
|
+
if (configOverrides && Object.keys(configOverrides).length > 0) {
|
|
65
|
+
resolved = deepMerge(resolved, configOverrides);
|
|
66
|
+
}
|
|
67
|
+
// Layer 2: instance-level overrides (per ThemeProvider)
|
|
68
|
+
if (instanceOverrides && Object.keys(instanceOverrides).length > 0) {
|
|
69
|
+
resolved = deepMerge(resolved, instanceOverrides);
|
|
70
|
+
}
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
// ─── Context & Provider ───────────────────────────────────────────────────────
|
|
74
|
+
const ThemeContext = createContext(defaultTheme);
|
|
75
|
+
export function ThemeProvider({ overrides, children }) {
|
|
76
|
+
const theme = useMemo(() => resolveTheme(overrides), [overrides]);
|
|
77
|
+
return _jsx(ThemeContext.Provider, { value: theme, children: children });
|
|
78
|
+
}
|
|
79
|
+
export function useTheme() {
|
|
80
|
+
return useContext(ThemeContext);
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ParsedDiagram, NodeMetadata } from '../types.js';
|
|
2
|
+
interface DiagramCanvasProps {
|
|
3
|
+
parsed: ParsedDiagram;
|
|
4
|
+
metadata?: Record<string, NodeMetadata>;
|
|
5
|
+
onNodeClick: (nodeId: string, nodeLabel: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function DiagramCanvas({ parsed, metadata, onNodeClick }: DiagramCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { GraphCanvas } from './GraphCanvas.js';
|
|
3
|
+
function UnsupportedCustomDiagram({ parsed }) {
|
|
4
|
+
return (_jsx("div", { style: {
|
|
5
|
+
display: 'flex',
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
justifyContent: 'center',
|
|
8
|
+
width: '100%',
|
|
9
|
+
height: '100%',
|
|
10
|
+
opacity: 0.8,
|
|
11
|
+
fontSize: 14,
|
|
12
|
+
}, children: `Diagram type '${parsed.diagramType}' is detected but custom rendering is not implemented yet.` }));
|
|
13
|
+
}
|
|
14
|
+
export function DiagramCanvas({ parsed, metadata, onNodeClick }) {
|
|
15
|
+
if (parsed.kind === 'graph') {
|
|
16
|
+
return _jsx(GraphCanvas, { parsed: parsed, metadata: metadata, onNodeClick: onNodeClick });
|
|
17
|
+
}
|
|
18
|
+
return _jsx(UnsupportedCustomDiagram, { parsed: parsed });
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import '@xyflow/react/dist/style.css';
|
|
2
|
+
import type { ParsedGraph, NodeMetadata } from '../types.js';
|
|
3
|
+
interface GraphCanvasProps {
|
|
4
|
+
parsed: ParsedGraph;
|
|
5
|
+
metadata?: Record<string, NodeMetadata>;
|
|
6
|
+
onNodeClick: (nodeId: string, nodeLabel: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function GraphCanvas(props: GraphCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useCallback, useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { ReactFlow, Background, Controls, MiniMap, ReactFlowProvider, useNodesInitialized, useStore, useStoreApi, } from '@xyflow/react';
|
|
4
|
+
import '@xyflow/react/dist/style.css';
|
|
5
|
+
import { layoutNodes, buildLayoutEdges } from '../layout/index.js';
|
|
6
|
+
import { getConfig, getNodeTypes } from '../config.js';
|
|
7
|
+
import { useTheme } from '../ThemeContext.js';
|
|
8
|
+
import { RoutedEdge } from './edges/RoutedEdge.js';
|
|
9
|
+
// Stable reference — must be outside the component to avoid infinite re-renders
|
|
10
|
+
const edgeTypes = { routed: RoutedEdge };
|
|
11
|
+
function GraphCanvasInner({ parsed, metadata, onNodeClick }) {
|
|
12
|
+
const theme = useTheme();
|
|
13
|
+
const { canvas } = getConfig();
|
|
14
|
+
const resolvedNodeTypes = getNodeTypes();
|
|
15
|
+
const storeApi = useStoreApi();
|
|
16
|
+
const nodesInitialized = useNodesInitialized();
|
|
17
|
+
const measuredSignature = useStore((s) => Array.from(s.nodeLookup.values()).reduce((hash, n) => {
|
|
18
|
+
const w = Math.round(n.measured?.width ?? n.width ?? 0);
|
|
19
|
+
const h = Math.round(n.measured?.height ?? n.height ?? 0);
|
|
20
|
+
const p = n.internals.positionAbsolute;
|
|
21
|
+
hash = ((hash << 5) - hash + n.id.length) | 0;
|
|
22
|
+
hash = ((hash << 5) - hash + Math.round(p.x)) | 0;
|
|
23
|
+
hash = ((hash << 5) - hash + Math.round(p.y)) | 0;
|
|
24
|
+
hash = ((hash << 5) - hash + w) | 0;
|
|
25
|
+
hash = ((hash << 5) - hash + h) | 0;
|
|
26
|
+
return hash;
|
|
27
|
+
}, 0));
|
|
28
|
+
// Phase 1: Layout nodes with dagre + initial edges with estimated dimensions
|
|
29
|
+
const layoutState = useMemo(() => {
|
|
30
|
+
try {
|
|
31
|
+
return {
|
|
32
|
+
result: layoutNodes(parsed, { theme, metadata }),
|
|
33
|
+
error: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
+
return { result: null, error: message };
|
|
39
|
+
}
|
|
40
|
+
}, [parsed, theme, metadata]);
|
|
41
|
+
const layoutResult = layoutState.result;
|
|
42
|
+
const nodes = layoutResult?.nodes ?? [];
|
|
43
|
+
const context = layoutResult?.context ?? undefined;
|
|
44
|
+
// Edges with estimated dimensions — always available immediately
|
|
45
|
+
const estimatedEdges = useMemo(() => context
|
|
46
|
+
? buildLayoutEdges(context, theme)
|
|
47
|
+
: (layoutResult?.edges ?? []), [context, theme, layoutResult]);
|
|
48
|
+
// Start with estimated edges so something is always visible
|
|
49
|
+
const [edges, setEdges] = useState(estimatedEdges);
|
|
50
|
+
const contextRef = useRef(context);
|
|
51
|
+
contextRef.current = context;
|
|
52
|
+
// When diagram changes, immediately show estimated edges
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setEdges(estimatedEdges);
|
|
55
|
+
}, [estimatedEdges]);
|
|
56
|
+
// Phase 2: Recompute when React Flow measurements change so intersections
|
|
57
|
+
// always use actual rendered node dimensions.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!nodesInitialized || !contextRef.current)
|
|
60
|
+
return;
|
|
61
|
+
const nodeLookup = storeApi.getState().nodeLookup;
|
|
62
|
+
const measuredPositions = new Map();
|
|
63
|
+
for (const internalNode of nodeLookup.values()) {
|
|
64
|
+
const estimated = contextRef.current.estimatedPositions.get(internalNode.id);
|
|
65
|
+
if (!estimated)
|
|
66
|
+
continue;
|
|
67
|
+
const measuredW = internalNode.measured?.width ?? internalNode.width ?? estimated.width;
|
|
68
|
+
const measuredH = internalNode.measured?.height ?? internalNode.height ?? estimated.height;
|
|
69
|
+
const abs = internalNode.internals.positionAbsolute;
|
|
70
|
+
measuredPositions.set(internalNode.id, {
|
|
71
|
+
x: abs.x,
|
|
72
|
+
y: abs.y,
|
|
73
|
+
width: measuredW,
|
|
74
|
+
height: measuredH,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
setEdges(buildLayoutEdges(contextRef.current, theme, measuredPositions));
|
|
78
|
+
}, [nodesInitialized, measuredSignature, theme, storeApi]);
|
|
79
|
+
const handleNodeClick = useCallback((_event, node) => {
|
|
80
|
+
const label = String(node.data?.label ?? node.id);
|
|
81
|
+
onNodeClick(node.id, label);
|
|
82
|
+
}, [onNodeClick]);
|
|
83
|
+
const defaultEdgeOptions = useMemo(() => ({ animated: false, style: theme.edgeDefaults.style }), [theme.edgeDefaults.style]);
|
|
84
|
+
const miniMapNodeColor = useCallback(() => theme.color.orange1, [theme.color.orange1]);
|
|
85
|
+
const miniMapMaskColor = useMemo(() => `${theme.color.gray1}cc`, [theme.color.gray1]);
|
|
86
|
+
if (layoutState.error || !layoutState.result) {
|
|
87
|
+
return (_jsx("div", { style: {
|
|
88
|
+
width: '100%',
|
|
89
|
+
height: '100%',
|
|
90
|
+
display: 'flex',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
justifyContent: 'center',
|
|
93
|
+
padding: 16,
|
|
94
|
+
color: theme.color.gray4,
|
|
95
|
+
fontFamily: theme.font.family,
|
|
96
|
+
fontSize: 13,
|
|
97
|
+
textAlign: 'center',
|
|
98
|
+
}, children: `Diagram syntax/layout error: ${layoutState.error ?? 'Unknown layout failure'}` }));
|
|
99
|
+
}
|
|
100
|
+
return (_jsx("div", { style: { width: '100%', height: '100%' }, children: _jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: resolvedNodeTypes, edgeTypes: edgeTypes, onNodeClick: handleNodeClick, fitView: canvas.fitView, minZoom: canvas.minZoom, maxZoom: canvas.maxZoom, defaultEdgeOptions: defaultEdgeOptions, children: [canvas.showBackground && (_jsx(Background, { color: theme.color.gray2, gap: canvas.backgroundGap, size: canvas.backgroundSize })), canvas.showControls && _jsx(Controls, {}), canvas.showMiniMap && (_jsx(MiniMap, { nodeColor: miniMapNodeColor, maskColor: miniMapMaskColor }))] }) }));
|
|
101
|
+
}
|
|
102
|
+
export function GraphCanvas(props) {
|
|
103
|
+
return (_jsx(ReactFlowProvider, { children: _jsx(GraphCanvasInner, { ...props }) }));
|
|
104
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NodeMetadata } from '../types.js';
|
|
2
|
+
interface NodeDetailProps {
|
|
3
|
+
nodeId: string;
|
|
4
|
+
nodeLabel: string;
|
|
5
|
+
meta: NodeMetadata | undefined;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare const NodeDetail: import("react").NamedExoticComponent<NodeDetailProps>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useMemo } from 'react';
|
|
3
|
+
import { renderMarkdown } from '../renderMarkdown.js';
|
|
4
|
+
import { useTheme } from '../ThemeContext.js';
|
|
5
|
+
import { getConfig } from '../config.js';
|
|
6
|
+
/**
|
|
7
|
+
* Generates scoped CSS for markdown content rendered via dangerouslySetInnerHTML.
|
|
8
|
+
* Uses a data-attribute selector for scoping so it won't leak into the consumer's app.
|
|
9
|
+
*/
|
|
10
|
+
function buildMarkdownStyles(theme) {
|
|
11
|
+
const scope = '[data-graph-detail-notes]';
|
|
12
|
+
return `
|
|
13
|
+
${scope} h1, ${scope} h2, ${scope} h3, ${scope} h4 {
|
|
14
|
+
color: ${theme.color.gray5};
|
|
15
|
+
margin: ${theme.space[4]} 0 ${theme.space[2]} 0;
|
|
16
|
+
line-height: ${theme.lineHeight.normal};
|
|
17
|
+
}
|
|
18
|
+
${scope} h1 { font-size: ${theme.font.size.xxl}; font-weight: ${theme.font.weight.bold}; }
|
|
19
|
+
${scope} h2 { font-size: ${theme.font.size.xl}; font-weight: ${theme.font.weight.semibold}; }
|
|
20
|
+
${scope} h3 { font-size: ${theme.font.size.lg}; font-weight: ${theme.font.weight.semibold}; }
|
|
21
|
+
${scope} h4 { font-size: ${theme.font.size.md}; font-weight: ${theme.font.weight.semibold}; text-transform: uppercase; letter-spacing: 0.3px; color: ${theme.color.gray3}; }
|
|
22
|
+
${scope} p {
|
|
23
|
+
color: ${theme.color.gray4};
|
|
24
|
+
font-size: ${theme.font.size.lg};
|
|
25
|
+
line-height: 1.6;
|
|
26
|
+
margin: 0 0 ${theme.space[3]} 0;
|
|
27
|
+
}
|
|
28
|
+
${scope} ul, ${scope} ol {
|
|
29
|
+
color: ${theme.color.gray4};
|
|
30
|
+
font-size: ${theme.font.size.lg};
|
|
31
|
+
line-height: 1.6;
|
|
32
|
+
padding-left: ${theme.space[5]};
|
|
33
|
+
margin: 0 0 ${theme.space[3]} 0;
|
|
34
|
+
}
|
|
35
|
+
${scope} li { margin-bottom: ${theme.space[1]}; }
|
|
36
|
+
${scope} code {
|
|
37
|
+
background-color: ${theme.color.gray1};
|
|
38
|
+
color: ${theme.color.orange1};
|
|
39
|
+
padding: 2px 6px;
|
|
40
|
+
border-radius: ${theme.radius.sm};
|
|
41
|
+
font-size: ${theme.font.size.base};
|
|
42
|
+
font-family: ${theme.font.mono};
|
|
43
|
+
}
|
|
44
|
+
${scope} pre { margin: ${theme.space[2]} 0 ${theme.space[3]} 0; }
|
|
45
|
+
${scope} pre code { background: none; color: ${theme.color.gray4}; padding: 0; }
|
|
46
|
+
${scope} strong { color: ${theme.color.gray5}; font-weight: ${theme.font.weight.semibold}; }
|
|
47
|
+
${scope} em { color: ${theme.color.gray4}; font-style: italic; }
|
|
48
|
+
${scope} a { color: ${theme.color.blue3}; text-decoration: none; }
|
|
49
|
+
${scope} a:hover { text-decoration: underline; }
|
|
50
|
+
${scope} hr { border: none; border-top: ${theme.borderWidth.sm} solid ${theme.color.gray2}; margin: ${theme.space[4]} 0; }
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
export const NodeDetail = memo(function NodeDetail({ nodeId, nodeLabel, meta, onClose }) {
|
|
54
|
+
const theme = useTheme();
|
|
55
|
+
const { library } = getConfig();
|
|
56
|
+
const detail = theme.nodeStyles.detail;
|
|
57
|
+
const status = meta?.status;
|
|
58
|
+
const statusStyle = status ? theme.statusColors[status] ?? { bg: theme.color.gray2, text: theme.color.gray4 } : null;
|
|
59
|
+
const hasNotes = meta?.notes && meta.notes.trim().length > 0;
|
|
60
|
+
const details = meta?.details;
|
|
61
|
+
const hasDetails = details && Object.keys(details).length > 0;
|
|
62
|
+
const hasContent = hasNotes || hasDetails || status;
|
|
63
|
+
const markdownCss = useMemo(() => hasNotes ? buildMarkdownStyles(theme) : '', [hasNotes, theme]);
|
|
64
|
+
return (_jsxs("div", { style: {
|
|
65
|
+
position: 'absolute', top: 0, right: 0,
|
|
66
|
+
width: 380, height: '100%',
|
|
67
|
+
backgroundColor: theme.color.darkBg2,
|
|
68
|
+
borderLeft: `${theme.borderWidth.sm} solid ${theme.color.gray2}`,
|
|
69
|
+
display: 'flex', flexDirection: 'column',
|
|
70
|
+
zIndex: 10,
|
|
71
|
+
fontFamily: theme.font.family,
|
|
72
|
+
animation: 'graphDetailSlideIn 0.2s ease-out',
|
|
73
|
+
}, children: [_jsx("style", { children: `
|
|
74
|
+
@keyframes graphDetailSlideIn {
|
|
75
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
76
|
+
to { transform: translateX(0); opacity: 1; }
|
|
77
|
+
}
|
|
78
|
+
${markdownCss}` }), _jsx("div", { style: {
|
|
79
|
+
display: 'flex', alignItems: 'center',
|
|
80
|
+
padding: `${theme.space[3]} ${theme.space[4]}`,
|
|
81
|
+
borderBottom: `${theme.borderWidth.sm} solid ${theme.color.gray2}`,
|
|
82
|
+
flexShrink: 0,
|
|
83
|
+
}, children: _jsxs("button", { onClick: onClose, style: {
|
|
84
|
+
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
|
85
|
+
background: 'none', border: 'none',
|
|
86
|
+
color: theme.color.gray3, fontSize: theme.font.size.base,
|
|
87
|
+
fontWeight: theme.font.weight.medium,
|
|
88
|
+
fontFamily: theme.font.family, cursor: 'pointer',
|
|
89
|
+
padding: `${theme.space[1]} ${theme.space[2]}`,
|
|
90
|
+
borderRadius: theme.radius.sm,
|
|
91
|
+
}, children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: _jsx("path", { d: "M10 12L6 8L10 4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }), "Back"] }) }), _jsxs("div", { style: {
|
|
92
|
+
flex: 1, overflowY: 'auto',
|
|
93
|
+
padding: `${theme.space[5]} ${theme.space[4]}`,
|
|
94
|
+
}, children: [_jsxs("div", { style: { marginBottom: theme.space[5] }, children: [_jsx("h2", { style: {
|
|
95
|
+
fontSize: theme.font.size.xxl, fontWeight: theme.font.weight.bold,
|
|
96
|
+
color: theme.color.white, margin: `0 0 ${theme.space[1]} 0`,
|
|
97
|
+
}, children: nodeLabel }), _jsx("span", { style: { fontSize: theme.font.size.md, color: theme.color.gray3, fontFamily: theme.font.mono }, children: nodeId })] }), statusStyle && status && (_jsxs("div", { style: {
|
|
98
|
+
display: 'inline-flex', alignItems: 'center', gap: theme.space[2],
|
|
99
|
+
padding: `${theme.space[1]} ${theme.space[3]}`,
|
|
100
|
+
borderRadius: theme.radius.stadium,
|
|
101
|
+
backgroundColor: statusStyle.bg, color: statusStyle.text,
|
|
102
|
+
fontSize: theme.font.size.md, fontWeight: theme.font.weight.semibold,
|
|
103
|
+
marginBottom: theme.space[5], textTransform: 'capitalize',
|
|
104
|
+
}, children: [_jsx("span", { style: {
|
|
105
|
+
width: detail.statusDotSize, height: detail.statusDotSize,
|
|
106
|
+
borderRadius: theme.radius.full,
|
|
107
|
+
backgroundColor: statusStyle.text,
|
|
108
|
+
} }), status] })), hasNotes && (_jsx("div", { "data-graph-detail-notes": "", dangerouslySetInnerHTML: { __html: renderMarkdown(meta.notes) } })), hasDetails && (_jsxs("div", { style: { marginTop: theme.space[5] }, children: [_jsx("h3", { style: {
|
|
109
|
+
fontSize: theme.font.size.xs, fontWeight: theme.font.weight.semibold, color: theme.color.gray3,
|
|
110
|
+
textTransform: 'uppercase', letterSpacing: detail.letterSpacing, marginBottom: theme.space[2],
|
|
111
|
+
}, children: "Details" }), _jsx("table", { style: { width: '100%', borderCollapse: 'collapse', fontSize: theme.font.size.base }, children: _jsx("tbody", { children: Object.entries(details).map(([key, value]) => (_jsxs("tr", { style: { borderBottom: `${theme.borderWidth.sm} solid ${theme.color.darkBg3}` }, children: [_jsx("td", { style: {
|
|
112
|
+
color: theme.color.gray3,
|
|
113
|
+
padding: `${theme.space[2]} ${theme.space[3]} ${theme.space[2]} 0`,
|
|
114
|
+
whiteSpace: 'nowrap', verticalAlign: 'top',
|
|
115
|
+
fontWeight: theme.font.weight.medium,
|
|
116
|
+
}, children: key }), _jsx("td", { style: {
|
|
117
|
+
color: theme.color.gray4,
|
|
118
|
+
padding: `${theme.space[2]} 0`,
|
|
119
|
+
wordBreak: 'break-word',
|
|
120
|
+
fontFamily: theme.font.mono,
|
|
121
|
+
fontSize: theme.font.size.md,
|
|
122
|
+
}, children: value })] }, key))) }) })] })), !hasContent && (_jsxs("div", { style: {
|
|
123
|
+
padding: `${theme.space[7]} ${theme.space[4]}`,
|
|
124
|
+
textAlign: 'center', color: theme.color.gray3,
|
|
125
|
+
fontSize: theme.font.size.lg, fontStyle: 'italic',
|
|
126
|
+
}, children: ["No metadata for this node.", _jsx("br", {}), _jsx("span", { style: { fontSize: theme.font.size.md, marginTop: theme.space[1], display: 'block' }, children: library.nodeDetailEmptyHint })] }))] })] }));
|
|
127
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoutedEdge — Renders a pre-computed SVG path from dagre waypoints,
|
|
3
|
+
* with optional ER cardinality markers (crow's foot notation).
|
|
4
|
+
*
|
|
5
|
+
* Instead of using SVG <marker> definitions (which have unreliable
|
|
6
|
+
* orient/refX behavior across browsers), we draw cardinality symbols
|
|
7
|
+
* as explicit SVG elements at the path endpoints using the tangent
|
|
8
|
+
* angle computed during edge routing.
|
|
9
|
+
*/
|
|
10
|
+
import type { EdgeProps } from '@xyflow/react';
|
|
11
|
+
export declare function RoutedEdge({ data, markerEnd, style, label, labelStyle, labelBgStyle, labelBgPadding, labelBgBorderRadius, }: EdgeProps): import("react/jsx-runtime").JSX.Element | null;
|