@graph-artifact/core 0.1.13 → 0.1.15
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/dist/components/GraphCanvas.d.ts +17 -4
- package/dist/components/GraphCanvas.js +136 -37
- package/dist/components/edges/RoutedEdge.d.ts +3 -4
- package/dist/components/edges/RoutedEdge.js +14 -7
- package/dist/components/exportPng.d.ts +46 -0
- package/dist/components/exportPng.js +156 -0
- package/dist/components/nodes/ClassNode.js +9 -2
- package/dist/components/nodes/EntityNode.js +41 -17
- package/dist/components/nodes/FlowNode.js +22 -7
- package/dist/components/nodes/StateNode.js +13 -17
- package/dist/components/nodes/SubgraphNode.js +2 -2
- package/dist/config.d.ts +1 -1
- package/dist/config.js +7 -7
- package/dist/layout/dagre/index.js +49 -0
- package/dist/layout/dagre/nodeSizing.js +8 -8
- package/dist/layout/edges/buildEdges.js +681 -25
- package/dist/layout/edges/paths.d.ts +4 -0
- package/dist/layout/edges/paths.js +137 -9
- package/dist/layout/index.js +19 -0
- package/dist/layout/sequenceLayout.js +5 -2
- package/dist/parsers/state.js +162 -23
- package/dist/theme/dark.js +11 -11
- package/dist/theme/light.js +10 -10
- package/dist/utils/boxShadow.d.ts +9 -0
- package/dist/utils/boxShadow.js +103 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
import { Handle, Position } from '@xyflow/react';
|
|
4
4
|
import { useTheme } from '../../ThemeContext.js';
|
|
5
|
+
import { getConfig } from '../../config.js';
|
|
5
6
|
const invisibleHandle = {
|
|
6
7
|
opacity: 0,
|
|
7
8
|
width: 1,
|
|
@@ -27,31 +28,54 @@ function EntityHandles({ fwIn, fwOut }) {
|
|
|
27
28
|
}
|
|
28
29
|
export const EntityNode = memo(function EntityNode({ data }) {
|
|
29
30
|
const theme = useTheme();
|
|
31
|
+
const { layout } = getConfig();
|
|
30
32
|
const label = String(data.label ?? '');
|
|
31
33
|
const attrs = data.attributes ?? [];
|
|
32
34
|
const ns = theme.nodeStyles.entity;
|
|
33
35
|
const fwIn = Math.max(1, data.fwIn ?? 1);
|
|
34
36
|
const fwOut = Math.max(1, data.fwOut ?? 1);
|
|
35
37
|
const layoutWidth = data.layoutWidth;
|
|
36
|
-
const
|
|
38
|
+
const minWidth = Number.isFinite(layoutWidth) && layoutWidth > 0
|
|
39
|
+
? layoutWidth
|
|
40
|
+
: layout.nodeSizing.common.minWidth;
|
|
37
41
|
return (_jsxs("div", { style: {
|
|
38
42
|
...theme.nodeBase.card,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const badge = parts[2];
|
|
45
|
-
const badgeVariant = badge === 'PK' ? 'primary' : badge === 'FK' ? 'secondary' : 'muted';
|
|
46
|
-
return (_jsxs("div", { style: {
|
|
43
|
+
boxSizing: 'border-box',
|
|
44
|
+
width: 'fit-content',
|
|
45
|
+
minWidth,
|
|
46
|
+
maxWidth: layout.nodeSizing.common.maxWidth,
|
|
47
|
+
}, children: [_jsxs("div", { style: { padding: `${theme.space[2]} ${theme.space[3]}` }, children: [_jsx("div", { style: {
|
|
47
48
|
display: 'flex', alignItems: 'center', gap: theme.space[2],
|
|
48
|
-
padding: `${theme.space[1]} 0`,
|
|
49
|
-
borderBottom: i < attrs.length - 1 ? `${theme.borderWidth.sm} solid ${theme.color.gray2}` : 'none',
|
|
50
49
|
fontSize: theme.font.size.md,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
backgroundColor: theme.color.orange1,
|
|
51
|
+
margin: `-${theme.space[2]} -${theme.space[3]}`,
|
|
52
|
+
marginBottom: attrs.length > 0 ? theme.space[1] : `-${theme.space[2]}`,
|
|
53
|
+
padding: `${theme.space[2]} ${theme.space[3]}`,
|
|
54
|
+
borderRadius: `${theme.radius.lg} ${theme.radius.lg} 0 0`,
|
|
55
|
+
}, children: _jsx("span", { style: {
|
|
56
|
+
color: theme.color.white,
|
|
57
|
+
fontWeight: theme.font.weight.semibold,
|
|
58
|
+
flex: 1,
|
|
59
|
+
minWidth: 0,
|
|
60
|
+
whiteSpace: 'nowrap',
|
|
61
|
+
overflow: 'hidden',
|
|
62
|
+
textOverflow: 'ellipsis',
|
|
63
|
+
textAlign: 'center',
|
|
64
|
+
}, title: label, children: label }) }), attrs.map((attr, i) => {
|
|
65
|
+
const parts = attr.split(' ');
|
|
66
|
+
const type = parts[0] ?? '';
|
|
67
|
+
const name = parts[1] ?? '';
|
|
68
|
+
const badge = parts[2];
|
|
69
|
+
const badgeVariant = badge === 'PK' ? 'primary' : badge === 'FK' ? 'secondary' : 'muted';
|
|
70
|
+
return (_jsxs("div", { style: {
|
|
71
|
+
display: 'flex', alignItems: 'center', gap: theme.space[2],
|
|
72
|
+
padding: `${theme.space[1]} 0`,
|
|
73
|
+
borderBottom: i < attrs.length - 1 ? `${theme.borderWidth.sm} solid ${theme.color.gray2}` : 'none',
|
|
74
|
+
fontSize: theme.font.size.md,
|
|
75
|
+
}, children: [_jsx("span", { style: {
|
|
76
|
+
color: theme.color.gray3, minWidth: ns.typeColumnWidth,
|
|
77
|
+
fontFamily: theme.font.mono, fontSize: theme.font.size.sm,
|
|
78
|
+
flexShrink: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
79
|
+
}, children: _jsx("span", { title: type, children: type }) }), _jsx("span", { style: { color: theme.color.gray4, flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, title: name, children: name }), badge && _jsx("span", { style: theme.nodeBase.badge(badgeVariant), children: badge })] }, i));
|
|
80
|
+
})] }), _jsx(EntityHandles, { fwIn: fwIn, fwOut: fwOut })] }));
|
|
57
81
|
});
|
|
@@ -72,13 +72,20 @@ function NodeHandles({ fwIn, fwOut }) {
|
|
|
72
72
|
* source handles are needed at each position (bottom, left, right) based on
|
|
73
73
|
* where each outgoing edge's target is located relative to the diamond center.
|
|
74
74
|
*
|
|
75
|
+
* `fwIn` specifies how many incoming edges need target handles (for spreading).
|
|
76
|
+
*
|
|
75
77
|
* Fallback: if no config is provided (single outgoing), uses bottom handle.
|
|
76
78
|
*/
|
|
77
|
-
function DiamondHandles({ handleConfig }) {
|
|
79
|
+
function DiamondHandles({ handleConfig, fwIn = 1 }) {
|
|
78
80
|
const bottom = handleConfig?.bottom ?? 1;
|
|
79
81
|
const left = handleConfig?.left ?? 0;
|
|
80
82
|
const right = handleConfig?.right ?? 0;
|
|
81
|
-
|
|
83
|
+
// Spread multiple incoming target handles across the top edge
|
|
84
|
+
const targetCount = Math.max(1, fwIn);
|
|
85
|
+
const targetPositions = targetCount === 1
|
|
86
|
+
? [50]
|
|
87
|
+
: Array.from({ length: targetCount }, (_, i) => 25 + (50 * i) / (targetCount - 1));
|
|
88
|
+
return (_jsxs(_Fragment, { children: [targetPositions.map((pct, i) => (_jsx(Handle, { type: "target", id: `fw-target-${i}`, position: Position.Top, style: { ...invisibleHandle, left: `${pct}%` } }, `dt-${i}`))), Array.from({ length: bottom }, (_, i) => (_jsx(Handle, { type: "source", id: `fw-source-bottom-${i}`, position: Position.Bottom, style: invisibleHandle }, `db-${i}`))), Array.from({ length: left }, (_, i) => (_jsx(Handle, { type: "source", id: `fw-source-left-${i}`, position: Position.Left, style: invisibleHandle }, `dl-${i}`))), Array.from({ length: right }, (_, i) => (_jsx(Handle, { type: "source", id: `fw-source-right-${i}`, position: Position.Right, style: invisibleHandle }, `dr-${i}`))), _jsx(Handle, { type: "source", id: "fw-source-0", position: Position.Bottom, style: invisibleHandle }), _jsx(SideHandles, { side: "left" }), _jsx(SideHandles, { side: "right" }), _jsx(Handle, { type: "target", id: "lateral-target", position: Position.Left, style: invisibleHandle }), _jsx(Handle, { type: "source", id: "lateral-source", position: Position.Right, style: invisibleHandle })] }));
|
|
82
89
|
}
|
|
83
90
|
export const FlowNode = memo(function FlowNode({ data }) {
|
|
84
91
|
const theme = useTheme();
|
|
@@ -94,7 +101,15 @@ export const FlowNode = memo(function FlowNode({ data }) {
|
|
|
94
101
|
const nodeBgColor = data.nodeBgColor ?? theme.color.orange1;
|
|
95
102
|
const nodeTextColor = data.nodeTextColor ?? theme.color.white;
|
|
96
103
|
const markdownComponents = {
|
|
97
|
-
p: ({ children }) => (_jsx("p", { style: {
|
|
104
|
+
p: ({ children }) => (_jsx("p", { style: {
|
|
105
|
+
margin: 0,
|
|
106
|
+
whiteSpace: 'pre-wrap',
|
|
107
|
+
lineHeight: theme.lineHeight.normal,
|
|
108
|
+
// Ensure long unbroken glyph runs (e.g. box-drawing lines) don't
|
|
109
|
+
// visually overflow fixed-width nodes, which breaks subgraph bounds.
|
|
110
|
+
overflowWrap: 'anywhere',
|
|
111
|
+
wordBreak: 'break-word',
|
|
112
|
+
}, children: children })),
|
|
98
113
|
strong: ({ children }) => (_jsx("strong", { style: { fontWeight: theme.font.weight.bold }, children: children })),
|
|
99
114
|
em: ({ children }) => (_jsx("em", { style: { fontStyle: 'italic' }, children: children })),
|
|
100
115
|
code: ({ children }) => (_jsx("code", { style: {
|
|
@@ -122,11 +137,11 @@ export const FlowNode = memo(function FlowNode({ data }) {
|
|
|
122
137
|
transform: 'rotate(45deg)', borderRadius: theme.radius.md,
|
|
123
138
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
124
139
|
}, children: _jsx("span", { style: {
|
|
125
|
-
transform: 'rotate(-45deg)', color: theme.color.white, fontSize: theme.font.size.
|
|
140
|
+
transform: 'rotate(-45deg)', color: theme.color.white, fontSize: theme.font.size.lg,
|
|
126
141
|
fontWeight: theme.font.weight.bold, fontFamily: theme.font.family,
|
|
127
142
|
textAlign: 'center', lineHeight: theme.lineHeight.tight,
|
|
128
143
|
padding: theme.space[2], maxWidth: ns.diamond.labelMaxWidth, overflowWrap: 'break-word',
|
|
129
|
-
}, children: _jsx(ReactMarkdown, { components: markdownComponents, children: labelMarkdown }) }) }), _jsx(DiamondHandles, { handleConfig: data.diamondHandleConfig })] }));
|
|
144
|
+
}, children: _jsx(ReactMarkdown, { components: markdownComponents, children: labelMarkdown }) }) }), _jsx(DiamondHandles, { handleConfig: data.diamondHandleConfig, fwIn: data.fwIn })] }));
|
|
130
145
|
}
|
|
131
146
|
if (shape === 'circle') {
|
|
132
147
|
return (_jsxs("div", { style: {
|
|
@@ -135,8 +150,8 @@ export const FlowNode = memo(function FlowNode({ data }) {
|
|
|
135
150
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
136
151
|
fontFamily: theme.font.family,
|
|
137
152
|
}, children: [_jsx(Handle, { type: "target", id: "fw-target-0", position: Position.Top, style: invisibleHandle }), _jsx("span", { style: {
|
|
138
|
-
color: nodeTextColor, fontSize: theme.font.size.
|
|
139
|
-
textAlign: 'center', padding: theme.space[
|
|
153
|
+
color: nodeTextColor, fontSize: theme.font.size.lg, fontWeight: theme.font.weight.bold,
|
|
154
|
+
textAlign: 'center', padding: theme.space[2], maxWidth: '100%', overflowWrap: 'break-word',
|
|
140
155
|
}, children: _jsx(ReactMarkdown, { components: markdownComponents, children: labelMarkdown }) }), _jsx(Handle, { type: "source", id: "fw-source-0", position: Position.Bottom, style: invisibleHandle }), _jsx(NodeHandles, { fwIn: fwIn, fwOut: fwOut })] }));
|
|
141
156
|
}
|
|
142
157
|
if (shape === 'cylinder') {
|
|
@@ -34,29 +34,25 @@ export const StateNode = memo(function StateNode({ data }) {
|
|
|
34
34
|
const fwOut = Math.max(1, data.fwOut ?? 1);
|
|
35
35
|
const layoutWidth = data.layoutWidth;
|
|
36
36
|
const layoutHeight = data.layoutHeight;
|
|
37
|
-
// Start state:
|
|
37
|
+
// Start state: ring with thick orange border and transparent center
|
|
38
38
|
if (shape === 'circle') {
|
|
39
39
|
return (_jsx("div", { style: {
|
|
40
|
-
width: layoutWidth,
|
|
41
|
-
|
|
40
|
+
width: layoutWidth,
|
|
41
|
+
height: layoutHeight,
|
|
42
|
+
boxSizing: 'border-box',
|
|
43
|
+
borderRadius: theme.radius.full,
|
|
44
|
+
border: `10px solid ${theme.color.orange1}`,
|
|
45
|
+
backgroundColor: 'transparent',
|
|
42
46
|
}, children: _jsx(StateHandles, { fwIn: fwIn, fwOut: fwOut }) }));
|
|
43
47
|
}
|
|
44
|
-
// End state:
|
|
45
|
-
// layoutWidth/layoutHeight include the border, so use box-sizing: border-box.
|
|
48
|
+
// End state: solid filled circle
|
|
46
49
|
if (shape === 'doublecircle') {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
return (_jsx("div", { style: {
|
|
51
|
+
width: layoutWidth,
|
|
52
|
+
height: layoutHeight,
|
|
53
|
+
backgroundColor: theme.color.orange1,
|
|
51
54
|
borderRadius: theme.radius.full,
|
|
52
|
-
|
|
53
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
54
|
-
}, children: [_jsx("div", { style: {
|
|
55
|
-
width: innerSize, height: innerSize,
|
|
56
|
-
// Inner fill: dark in dark mode, dark gray in light mode.
|
|
57
|
-
backgroundColor: theme.mode === 'dark' ? theme.color.darkBg2 : theme.color.gray5,
|
|
58
|
-
borderRadius: theme.radius.full,
|
|
59
|
-
} }), _jsx(StateHandles, { fwIn: fwIn, fwOut: fwOut })] }));
|
|
55
|
+
}, children: _jsx(StateHandles, { fwIn: fwIn, fwOut: fwOut }) }));
|
|
60
56
|
}
|
|
61
57
|
return (_jsxs("div", { style: {
|
|
62
58
|
backgroundColor: theme.color.orange1,
|
|
@@ -6,10 +6,10 @@ export const SubgraphNode = memo(function SubgraphNode({ data }) {
|
|
|
6
6
|
const label = String(data.label ?? '');
|
|
7
7
|
const ns = theme.nodeStyles.subgraph;
|
|
8
8
|
return (_jsx("div", { style: {
|
|
9
|
-
fontSize: theme.font.size.
|
|
9
|
+
fontSize: theme.font.size.base,
|
|
10
10
|
fontWeight: theme.font.weight.semibold,
|
|
11
11
|
fontFamily: theme.font.family,
|
|
12
|
-
color: theme.color.
|
|
12
|
+
color: theme.mode === 'dark' ? theme.color.white : theme.color.gray5,
|
|
13
13
|
padding: `${theme.space[1]} ${theme.space[2]}`,
|
|
14
14
|
pointerEvents: ns.pointerEvents,
|
|
15
15
|
}, children: label }));
|
package/dist/config.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Consumers call `configure()` once at module load time. Every internal module
|
|
9
9
|
* reads from `getConfig()`. Nothing is hardcoded elsewhere.
|
|
10
10
|
*/
|
|
11
|
-
import type {
|
|
11
|
+
import type { NodeChildComponent, NodeComponent, ThemeOverride } from './types.js';
|
|
12
12
|
export interface GraphConfig {
|
|
13
13
|
/** Which theme to use as the base token set ('dark', 'light', or any registered name) */
|
|
14
14
|
baseTheme: string;
|
package/dist/config.js
CHANGED
|
@@ -15,11 +15,11 @@ function createDefaultConfig() {
|
|
|
15
15
|
theme: {},
|
|
16
16
|
layout: {
|
|
17
17
|
rankSep: 30,
|
|
18
|
-
erRankSep:
|
|
19
|
-
stateRankSep:
|
|
18
|
+
erRankSep: 100,
|
|
19
|
+
stateRankSep: 25,
|
|
20
20
|
nodeSep: 30,
|
|
21
|
-
erNodeSep:
|
|
22
|
-
stateNodeSep:
|
|
21
|
+
erNodeSep: 120,
|
|
22
|
+
stateNodeSep: 20,
|
|
23
23
|
subgraphPadding: 16,
|
|
24
24
|
nodeSizing: {
|
|
25
25
|
charWidth: 9,
|
|
@@ -27,11 +27,11 @@ function createDefaultConfig() {
|
|
|
27
27
|
er: { width: 220, headerHeight: 33, rowHeight: 23, borderExtra: 2 },
|
|
28
28
|
class: { width: 240, headerHeight: 33, rowHeight: 20, dividerGap: 17 },
|
|
29
29
|
state: {
|
|
30
|
-
circleSize:
|
|
30
|
+
circleSize: 36, doublecirclePad: 0, doublecircleBorder: 0,
|
|
31
31
|
minWidth: 100, height: 40, padding: 32, borderExtra: 6,
|
|
32
32
|
},
|
|
33
33
|
flow: {
|
|
34
|
-
diamondSize:
|
|
34
|
+
diamondSize: 150, circleSize: 100, cylinderWidth: 120, cylinderHeight: 70,
|
|
35
35
|
minWidth: 120, height: 44, padding: 40,
|
|
36
36
|
},
|
|
37
37
|
},
|
|
@@ -45,7 +45,7 @@ function createDefaultConfig() {
|
|
|
45
45
|
labelGapBelow: 6,
|
|
46
46
|
topPadding: 20,
|
|
47
47
|
leftPadding: 40,
|
|
48
|
-
lifelineStartOffset:
|
|
48
|
+
lifelineStartOffset: 0, // Lifelines connect directly to participant bottom
|
|
49
49
|
bottomParticipantGap: 40,
|
|
50
50
|
anchorWidth: 2,
|
|
51
51
|
anchorHeight: 2,
|
|
@@ -112,6 +112,48 @@ export function createDagreGraph(parsed, layoutConfig, nodeSizeOverrides) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
// Calculate actual depth (longest path) for each subgraph
|
|
116
|
+
const subgraphDepth = new Map();
|
|
117
|
+
if (parsed.subgraphs) {
|
|
118
|
+
for (const sg of parsed.subgraphs) {
|
|
119
|
+
const internalNodes = new Set(sg.nodeIds);
|
|
120
|
+
// Build adjacency list for internal edges
|
|
121
|
+
const adj = new Map();
|
|
122
|
+
for (const nodeId of sg.nodeIds) {
|
|
123
|
+
adj.set(nodeId, []);
|
|
124
|
+
}
|
|
125
|
+
for (const edge of parsed.edges) {
|
|
126
|
+
if (internalNodes.has(edge.source) && internalNodes.has(edge.target)) {
|
|
127
|
+
adj.get(edge.source)?.push(edge.target);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Find longest path using DFS with cycle detection
|
|
131
|
+
const memo = new Map();
|
|
132
|
+
const visiting = new Set();
|
|
133
|
+
function longestPath(node) {
|
|
134
|
+
if (memo.has(node))
|
|
135
|
+
return memo.get(node);
|
|
136
|
+
if (visiting.has(node))
|
|
137
|
+
return 0; // Cycle detected, stop recursion
|
|
138
|
+
visiting.add(node);
|
|
139
|
+
const neighbors = adj.get(node) ?? [];
|
|
140
|
+
let maxDepth = 0;
|
|
141
|
+
for (const neighbor of neighbors) {
|
|
142
|
+
maxDepth = Math.max(maxDepth, 1 + longestPath(neighbor));
|
|
143
|
+
}
|
|
144
|
+
visiting.delete(node);
|
|
145
|
+
memo.set(node, maxDepth);
|
|
146
|
+
return maxDepth;
|
|
147
|
+
}
|
|
148
|
+
// Find max depth from any starting node
|
|
149
|
+
let maxDepth = 0;
|
|
150
|
+
for (const nodeId of sg.nodeIds) {
|
|
151
|
+
maxDepth = Math.max(maxDepth, longestPath(nodeId));
|
|
152
|
+
}
|
|
153
|
+
// Just use the depth + 1 for minimal clearance
|
|
154
|
+
subgraphDepth.set(sg.id, maxDepth + 1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
115
157
|
parsed.edges.forEach((edge, i) => {
|
|
116
158
|
// Pass label dimensions to dagre so it spaces edges apart
|
|
117
159
|
// and generates better waypoints. Without this, dagre treats
|
|
@@ -126,12 +168,19 @@ export function createDagreGraph(parsed, layoutConfig, nodeSizeOverrides) {
|
|
|
126
168
|
g.setNode(dagreSrc, fallbackNode);
|
|
127
169
|
if (!g.hasNode(dagreTgt))
|
|
128
170
|
g.setNode(dagreTgt, fallbackNode);
|
|
171
|
+
// For edges FROM a subgraph, add minlen based on actual subgraph depth
|
|
172
|
+
let minlen = 1;
|
|
173
|
+
if (edge.sourceSubgraphId) {
|
|
174
|
+
// Use calculated depth to push target just below subgraph
|
|
175
|
+
minlen = subgraphDepth.get(edge.sourceSubgraphId) ?? 2;
|
|
176
|
+
}
|
|
129
177
|
// Use index as edge name for multigraph — prevents overwriting when
|
|
130
178
|
// multiple edges exist between the same source→target pair.
|
|
131
179
|
g.setEdge(dagreSrc, dagreTgt, {
|
|
132
180
|
width: labelWidth,
|
|
133
181
|
height: labelHeight,
|
|
134
182
|
labelpos: 'c',
|
|
183
|
+
minlen,
|
|
135
184
|
}, `e${i}`);
|
|
136
185
|
});
|
|
137
186
|
try {
|
|
@@ -134,13 +134,14 @@ function erNodeSize(node, sizing) {
|
|
|
134
134
|
const attrCount = node.attributes?.length ?? 0;
|
|
135
135
|
const attrs = node.attributes ?? [];
|
|
136
136
|
// Height breakdown (mirrors EntityNode CSS):
|
|
137
|
-
//
|
|
138
|
-
//
|
|
137
|
+
// sectionPad: wrapper padding (space[2]*2 = 8+8 = 16px)
|
|
138
|
+
// headerRow: header row height (same as attribute rows)
|
|
139
139
|
// rowHeight: per-row padding (space[1]*2 = 4+4) + font (size.md) * line-height
|
|
140
|
-
// dividers: 1px borderBottom between
|
|
140
|
+
// dividers: 1px borderBottom between rows (header + attrs - 1)
|
|
141
141
|
// borderExtra: card border (borderWidth.sm * 2 = 1+1 = 2px)
|
|
142
|
-
const sectionPad =
|
|
143
|
-
const
|
|
142
|
+
const sectionPad = 16; // always have wrapper padding now
|
|
143
|
+
const totalRows = 1 + attrCount; // header + attributes
|
|
144
|
+
const dividers = totalRows > 1 ? totalRows - 1 : 0;
|
|
144
145
|
const badgeRoom = attrs.some((attr) => {
|
|
145
146
|
const t = attr.trim().split(/\s+/);
|
|
146
147
|
return t[2] === 'PK' || t[2] === 'FK' || t[2] === 'UK';
|
|
@@ -157,9 +158,8 @@ function erNodeSize(node, sizing) {
|
|
|
157
158
|
const estimatedWidth = Math.max(sizing.er.width, 24 + 90 + 8 + contentChars * sizing.charWidth + badgeRoom);
|
|
158
159
|
return {
|
|
159
160
|
width: clampWidth(estimatedWidth),
|
|
160
|
-
height:
|
|
161
|
-
+
|
|
162
|
-
+ attrCount * sizing.er.rowHeight
|
|
161
|
+
height: sectionPad
|
|
162
|
+
+ totalRows * sizing.er.rowHeight
|
|
163
163
|
+ dividers
|
|
164
164
|
+ sizing.er.borderExtra,
|
|
165
165
|
};
|