@flanksource/clicky-ui 0.3.45 → 0.3.46
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/README.md +22 -0
- package/dist/data/AccessMark.cjs +23 -0
- package/dist/data/AccessMark.cjs.map +1 -0
- package/dist/data/AccessMark.d.ts +11 -0
- package/dist/data/AccessMark.d.ts.map +1 -0
- package/dist/data/AccessMark.js +23 -0
- package/dist/data/AccessMark.js.map +1 -0
- package/dist/data/CodeDiff.cjs +116 -23
- package/dist/data/CodeDiff.cjs.map +1 -1
- package/dist/data/CodeDiff.d.ts +26 -0
- package/dist/data/CodeDiff.d.ts.map +1 -1
- package/dist/data/CodeDiff.js +116 -23
- package/dist/data/CodeDiff.js.map +1 -1
- package/dist/data/GraphDiagram.cjs +65 -13
- package/dist/data/GraphDiagram.cjs.map +1 -1
- package/dist/data/GraphDiagram.d.ts +9 -2
- package/dist/data/GraphDiagram.d.ts.map +1 -1
- package/dist/data/GraphDiagram.js +66 -14
- package/dist/data/GraphDiagram.js.map +1 -1
- package/dist/data/GraphDiagramGroups.cjs +100 -0
- package/dist/data/GraphDiagramGroups.cjs.map +1 -0
- package/dist/data/GraphDiagramGroups.d.ts +24 -0
- package/dist/data/GraphDiagramGroups.d.ts.map +1 -0
- package/dist/data/GraphDiagramGroups.js +100 -0
- package/dist/data/GraphDiagramGroups.js.map +1 -0
- package/dist/data/GraphDiagramNodes.cjs +17 -31
- package/dist/data/GraphDiagramNodes.cjs.map +1 -1
- package/dist/data/GraphDiagramNodes.d.ts +6 -13
- package/dist/data/GraphDiagramNodes.d.ts.map +1 -1
- package/dist/data/GraphDiagramNodes.js +17 -31
- package/dist/data/GraphDiagramNodes.js.map +1 -1
- package/dist/data/TreeNode.cjs +1 -1
- package/dist/data/TreeNode.cjs.map +1 -1
- package/dist/data/TreeNode.d.ts.map +1 -1
- package/dist/data/TreeNode.js +1 -1
- package/dist/data/TreeNode.js.map +1 -1
- package/dist/data/call-graph/CallGraph.cjs +50 -31
- package/dist/data/call-graph/CallGraph.cjs.map +1 -1
- package/dist/data/call-graph/CallGraph.d.ts +10 -1
- package/dist/data/call-graph/CallGraph.d.ts.map +1 -1
- package/dist/data/call-graph/CallGraph.js +52 -33
- package/dist/data/call-graph/CallGraph.js.map +1 -1
- package/dist/data/call-graph/CallGraphMembers.cjs +5 -4
- package/dist/data/call-graph/CallGraphMembers.cjs.map +1 -1
- package/dist/data/call-graph/CallGraphMembers.d.ts +1 -1
- package/dist/data/call-graph/CallGraphMembers.d.ts.map +1 -1
- package/dist/data/call-graph/CallGraphMembers.js +5 -4
- package/dist/data/call-graph/CallGraphMembers.js.map +1 -1
- package/dist/data/call-graph/CallGraphToolbar.cjs +17 -5
- package/dist/data/call-graph/CallGraphToolbar.cjs.map +1 -1
- package/dist/data/call-graph/CallGraphToolbar.d.ts +10 -0
- package/dist/data/call-graph/CallGraphToolbar.d.ts.map +1 -1
- package/dist/data/call-graph/CallGraphToolbar.js +18 -6
- package/dist/data/call-graph/CallGraphToolbar.js.map +1 -1
- package/dist/data/call-graph/call-graph-model.cjs +169 -18
- package/dist/data/call-graph/call-graph-model.cjs.map +1 -1
- package/dist/data/call-graph/call-graph-model.d.ts +40 -4
- package/dist/data/call-graph/call-graph-model.d.ts.map +1 -1
- package/dist/data/call-graph/call-graph-model.js +170 -19
- package/dist/data/call-graph/call-graph-model.js.map +1 -1
- package/dist/data/call-graph/call-graph-vocabulary.cjs +19 -1
- package/dist/data/call-graph/call-graph-vocabulary.cjs.map +1 -1
- package/dist/data/call-graph/call-graph-vocabulary.d.ts +4 -0
- package/dist/data/call-graph/call-graph-vocabulary.d.ts.map +1 -1
- package/dist/data/call-graph/call-graph-vocabulary.js +19 -1
- package/dist/data/call-graph/call-graph-vocabulary.js.map +1 -1
- package/dist/data/call-graph/call-graph.fixtures.d.ts.map +1 -1
- package/dist/data/call-graph/index.d.ts +2 -2
- package/dist/data/call-graph/index.d.ts.map +1 -1
- package/dist/data/call-graph/types.cjs.map +1 -1
- package/dist/data/call-graph/types.d.ts +7 -2
- package/dist/data/call-graph/types.d.ts.map +1 -1
- package/dist/data/call-graph/types.js.map +1 -1
- package/dist/data/data-access.cjs +8 -0
- package/dist/data/data-access.cjs.map +1 -0
- package/dist/data/data-access.d.ts +5 -0
- package/dist/data/data-access.d.ts.map +1 -0
- package/dist/data/data-access.js +8 -0
- package/dist/data/data-access.js.map +1 -0
- package/dist/data/git/GitChangeList.cjs +6 -2
- package/dist/data/git/GitChangeList.cjs.map +1 -1
- package/dist/data/git/GitChangeList.d.ts +6 -1
- package/dist/data/git/GitChangeList.d.ts.map +1 -1
- package/dist/data/git/GitChangeList.js +6 -2
- package/dist/data/git/GitChangeList.js.map +1 -1
- package/dist/data/graph-columns-layout.cjs +47 -16
- package/dist/data/graph-columns-layout.cjs.map +1 -1
- package/dist/data/graph-columns-layout.d.ts +34 -0
- package/dist/data/graph-columns-layout.d.ts.map +1 -1
- package/dist/data/graph-columns-layout.js +47 -16
- package/dist/data/graph-columns-layout.js.map +1 -1
- package/dist/data/graph-diagram-model.cjs +5 -0
- package/dist/data/graph-diagram-model.cjs.map +1 -1
- package/dist/data/graph-diagram-model.d.ts +19 -0
- package/dist/data/graph-diagram-model.d.ts.map +1 -1
- package/dist/data/graph-diagram-model.js +5 -0
- package/dist/data/graph-diagram-model.js.map +1 -1
- package/dist/data/graph-group-window.cjs +60 -0
- package/dist/data/graph-group-window.cjs.map +1 -0
- package/dist/data/graph-group-window.d.ts +33 -0
- package/dist/data/graph-group-window.d.ts.map +1 -0
- package/dist/data/graph-group-window.js +60 -0
- package/dist/data/graph-group-window.js.map +1 -0
- package/dist/data/use-group-windows.cjs +50 -0
- package/dist/data/use-group-windows.cjs.map +1 -0
- package/dist/data/use-group-windows.d.ts +27 -0
- package/dist/data/use-group-windows.d.ts.map +1 -0
- package/dist/data/use-group-windows.js +50 -0
- package/dist/data/use-group-windows.js.map +1 -0
- package/dist/data/version-info.cjs +3 -3
- package/dist/data/version-info.js +3 -3
- package/dist/data.cjs +10 -0
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.ts +4 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +13 -3
- package/dist/data.js.map +1 -1
- package/dist/hooks/event-hub.cjs +229 -0
- package/dist/hooks/event-hub.cjs.map +1 -0
- package/dist/hooks/event-hub.d.ts +26 -0
- package/dist/hooks/event-hub.d.ts.map +1 -0
- package/dist/hooks/event-hub.js +229 -0
- package/dist/hooks/event-hub.js.map +1 -0
- package/dist/hooks.cjs +2 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/overlay/modalStack.cjs +13 -6
- package/dist/overlay/modalStack.cjs.map +1 -1
- package/dist/overlay/modalStack.d.ts.map +1 -1
- package/dist/overlay/modalStack.js +13 -6
- package/dist/overlay/modalStack.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/dist/data/CodeDiff.js
CHANGED
|
@@ -8,6 +8,7 @@ import { HighlightedTokens } from "./HighlightedTokens.js";
|
|
|
8
8
|
import { UiRows } from "../icons/components/UiRows.js";
|
|
9
9
|
import { UiColumns } from "../icons/components/UiColumns.js";
|
|
10
10
|
import { UiFile } from "../icons/components/UiFile.js";
|
|
11
|
+
import { UiAdd } from "../icons/components/UiAdd.js";
|
|
11
12
|
const DIFF_VIEW_OPTIONS = [
|
|
12
13
|
{ id: "unified", label: "Unified", icon: UiRows, title: "Unified view" },
|
|
13
14
|
{ id: "split", label: "Split", icon: UiColumns, title: "Split view" }
|
|
@@ -25,7 +26,8 @@ const MARKER_TONE = {
|
|
|
25
26
|
const MARKER = { context: "", add: "+", remove: "-" };
|
|
26
27
|
function CodeDiff(props) {
|
|
27
28
|
const { language, view: viewProp = "unified", showLineNumbers = true, bare = false, className } = props;
|
|
28
|
-
const { unified, original, modified } = props;
|
|
29
|
+
const { unified, original, modified, onLineAction, lineWidgets } = props;
|
|
30
|
+
const annotations = { onLineAction, lineWidgets };
|
|
29
31
|
const [view, setView] = useState(viewProp);
|
|
30
32
|
const hunks = useMemo(
|
|
31
33
|
() => unified !== void 0 ? parseUnifiedDiff(unified) : computeLineDiff(original ?? "", modified ?? ""),
|
|
@@ -60,7 +62,7 @@ function CodeDiff(props) {
|
|
|
60
62
|
lastPath = hunk.path;
|
|
61
63
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
62
64
|
filePath && /* @__PURE__ */ jsx(FileHeader, { path: filePath }),
|
|
63
|
-
view === "split" ? /* @__PURE__ */ jsx(SplitHunk, { hunk, showLineNumbers }) : /* @__PURE__ */ jsx(UnifiedHunk, { hunk, showLineNumbers })
|
|
65
|
+
view === "split" ? /* @__PURE__ */ jsx(SplitHunk, { hunk, showLineNumbers, annotations }) : /* @__PURE__ */ jsx(UnifiedHunk, { hunk, showLineNumbers, annotations })
|
|
64
66
|
] }, index);
|
|
65
67
|
});
|
|
66
68
|
if (bare) {
|
|
@@ -112,32 +114,96 @@ function resolveHunks(hunks, oldTokens, newTokens) {
|
|
|
112
114
|
})
|
|
113
115
|
}));
|
|
114
116
|
}
|
|
115
|
-
function
|
|
117
|
+
function lineHasPosition(line, side, number) {
|
|
118
|
+
if (side === "old") return line.type !== "add" && line.oldNumber === number;
|
|
119
|
+
return line.type !== "remove" && line.newNumber === number;
|
|
120
|
+
}
|
|
121
|
+
function WidgetRows({ widgets }) {
|
|
122
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: widgets.map((widget) => /* @__PURE__ */ jsx(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
"data-diff-widget": widget.key,
|
|
126
|
+
className: "whitespace-normal border-y border-border bg-background px-3 py-2 font-sans text-sm leading-normal text-foreground",
|
|
127
|
+
children: widget.node
|
|
128
|
+
},
|
|
129
|
+
widget.key
|
|
130
|
+
)) });
|
|
131
|
+
}
|
|
132
|
+
function UnifiedHunk({ hunk, showLineNumbers, annotations }) {
|
|
133
|
+
const { onLineAction, lineWidgets } = annotations;
|
|
116
134
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
117
135
|
hunk.header && /* @__PURE__ */ jsx(HunkHeader, { header: hunk.header }),
|
|
118
136
|
hunk.lines.map((resolved, index) => {
|
|
119
137
|
const { line } = resolved;
|
|
120
138
|
const tokens = line.type === "remove" ? resolved.oldTokens : resolved.newTokens;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
const actionSide = line.type === "remove" ? "old" : "new";
|
|
140
|
+
const widgets = lineWidgets == null ? void 0 : lineWidgets.filter((w) => lineHasPosition(line, w.side, w.line));
|
|
141
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
142
|
+
/* @__PURE__ */ jsxs("div", { className: cn("group flex", ROW_TINT[line.type]), "data-diff-line": line.type, children: [
|
|
143
|
+
showLineNumbers && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
144
|
+
/* @__PURE__ */ jsx(
|
|
145
|
+
Gutter,
|
|
146
|
+
{
|
|
147
|
+
value: line.oldNumber,
|
|
148
|
+
line,
|
|
149
|
+
side: "old",
|
|
150
|
+
actionSide,
|
|
151
|
+
onLineAction
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
155
|
+
Gutter,
|
|
156
|
+
{
|
|
157
|
+
value: line.newNumber,
|
|
158
|
+
line,
|
|
159
|
+
side: "new",
|
|
160
|
+
actionSide,
|
|
161
|
+
onLineAction
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
] }),
|
|
165
|
+
/* @__PURE__ */ jsx(Marker, { type: line.type }),
|
|
166
|
+
/* @__PURE__ */ jsx("code", { className: "whitespace-pre pl-1 pr-3", children: /* @__PURE__ */ jsx(HighlightedTokens, { tokens, content: line.content }) })
|
|
125
167
|
] }),
|
|
126
|
-
/* @__PURE__ */ jsx(
|
|
127
|
-
/* @__PURE__ */ jsx("code", { className: "whitespace-pre pl-1 pr-3", children: /* @__PURE__ */ jsx(HighlightedTokens, { tokens, content: line.content }) })
|
|
168
|
+
widgets && /* @__PURE__ */ jsx(WidgetRows, { widgets })
|
|
128
169
|
] }, index);
|
|
129
170
|
})
|
|
130
171
|
] });
|
|
131
172
|
}
|
|
132
|
-
function SplitHunk({ hunk, showLineNumbers }) {
|
|
173
|
+
function SplitHunk({ hunk, showLineNumbers, annotations }) {
|
|
133
174
|
const pairs = useMemo(() => pairLines(hunk.lines), [hunk.lines]);
|
|
175
|
+
const { onLineAction, lineWidgets } = annotations;
|
|
134
176
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
135
177
|
hunk.header && /* @__PURE__ */ jsx(HunkHeader, { header: hunk.header }),
|
|
136
|
-
pairs.map((pair, index) =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
178
|
+
pairs.map((pair, index) => {
|
|
179
|
+
const widgets = lineWidgets == null ? void 0 : lineWidgets.filter(
|
|
180
|
+
(w) => pair.left !== void 0 && lineHasPosition(pair.left.line, w.side, w.line) && w.side === "old" || pair.right !== void 0 && lineHasPosition(pair.right.line, w.side, w.line) && w.side === "new"
|
|
181
|
+
);
|
|
182
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
183
|
+
/* @__PURE__ */ jsxs("div", { className: "flex", children: [
|
|
184
|
+
/* @__PURE__ */ jsx(
|
|
185
|
+
SplitCell,
|
|
186
|
+
{
|
|
187
|
+
resolved: pair.left,
|
|
188
|
+
side: "old",
|
|
189
|
+
showLineNumbers,
|
|
190
|
+
onLineAction
|
|
191
|
+
}
|
|
192
|
+
),
|
|
193
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "w-px shrink-0 bg-border" }),
|
|
194
|
+
/* @__PURE__ */ jsx(
|
|
195
|
+
SplitCell,
|
|
196
|
+
{
|
|
197
|
+
resolved: pair.right,
|
|
198
|
+
side: "new",
|
|
199
|
+
showLineNumbers,
|
|
200
|
+
onLineAction
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
] }),
|
|
204
|
+
widgets && /* @__PURE__ */ jsx(WidgetRows, { widgets })
|
|
205
|
+
] }, index);
|
|
206
|
+
})
|
|
141
207
|
] });
|
|
142
208
|
}
|
|
143
209
|
function pairLines(lines) {
|
|
@@ -164,7 +230,8 @@ function pairLines(lines) {
|
|
|
164
230
|
function SplitCell({
|
|
165
231
|
resolved,
|
|
166
232
|
side,
|
|
167
|
-
showLineNumbers
|
|
233
|
+
showLineNumbers,
|
|
234
|
+
onLineAction
|
|
168
235
|
}) {
|
|
169
236
|
if (!resolved) return /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1" });
|
|
170
237
|
const { line } = resolved;
|
|
@@ -172,11 +239,18 @@ function SplitCell({
|
|
|
172
239
|
const type = active ? line.type : "context";
|
|
173
240
|
const number = side === "old" ? line.oldNumber : line.newNumber;
|
|
174
241
|
const tokens = side === "old" ? resolved.oldTokens : resolved.newTokens;
|
|
175
|
-
return /* @__PURE__ */ jsxs(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
242
|
+
return /* @__PURE__ */ jsxs(
|
|
243
|
+
"div",
|
|
244
|
+
{
|
|
245
|
+
className: cn("group flex min-w-0 flex-1", active && ROW_TINT[line.type]),
|
|
246
|
+
"data-diff-line": type,
|
|
247
|
+
children: [
|
|
248
|
+
showLineNumbers && /* @__PURE__ */ jsx(Gutter, { value: number, line, side, actionSide: side, onLineAction }),
|
|
249
|
+
/* @__PURE__ */ jsx(Marker, { type }),
|
|
250
|
+
/* @__PURE__ */ jsx("code", { className: "min-w-0 whitespace-pre pl-1 pr-3", children: /* @__PURE__ */ jsx(HighlightedTokens, { tokens, content: line.content }) })
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
);
|
|
180
254
|
}
|
|
181
255
|
function FileHeader({ path }) {
|
|
182
256
|
return /* @__PURE__ */ jsxs(
|
|
@@ -194,8 +268,27 @@ function FileHeader({ path }) {
|
|
|
194
268
|
function HunkHeader({ header }) {
|
|
195
269
|
return /* @__PURE__ */ jsx("div", { className: "select-none bg-muted/60 px-2 py-0.5 text-muted-foreground", children: header });
|
|
196
270
|
}
|
|
197
|
-
function Gutter({
|
|
198
|
-
|
|
271
|
+
function Gutter({
|
|
272
|
+
value,
|
|
273
|
+
line,
|
|
274
|
+
side,
|
|
275
|
+
actionSide,
|
|
276
|
+
onLineAction
|
|
277
|
+
}) {
|
|
278
|
+
const showAction = onLineAction !== void 0 && value !== void 0 && side === actionSide;
|
|
279
|
+
return /* @__PURE__ */ jsxs("span", { className: "relative w-10 shrink-0 select-none px-2 text-right tabular-nums text-muted-foreground", children: [
|
|
280
|
+
value ?? "",
|
|
281
|
+
showAction && /* @__PURE__ */ jsx(
|
|
282
|
+
"button",
|
|
283
|
+
{
|
|
284
|
+
type: "button",
|
|
285
|
+
"aria-label": `Comment on line ${value}`,
|
|
286
|
+
onClick: () => onLineAction({ side, line: value, content: line.content }),
|
|
287
|
+
className: "absolute inset-y-0 left-0.5 flex items-center rounded-sm bg-background text-primary opacity-0 hover:text-primary/80 focus-visible:opacity-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring group-hover:opacity-100",
|
|
288
|
+
children: /* @__PURE__ */ jsx(UiAdd, { className: "size-3.5" })
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
] });
|
|
199
292
|
}
|
|
200
293
|
function Marker({ type }) {
|
|
201
294
|
return /* @__PURE__ */ jsx("span", { className: cn("w-4 shrink-0 select-none text-center", MARKER_TONE[type]), children: MARKER[type] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeDiff.js","sources":["../../src/data/CodeDiff.tsx"],"sourcesContent":["import { Fragment, useEffect, useMemo, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { SegmentedControl, type SegmentedOption } from \"../components/SegmentedControl\";\nimport { UiColumns, UiFile, UiRows } from \"../icons\";\nimport {\n computeLineDiff,\n parseUnifiedDiff,\n type DiffHunk,\n type DiffLine,\n type DiffLineType,\n} from \"./code-diff\";\nimport { highlightToLines, type HighlightedLine } from \"./code-highlight\";\nimport { HighlightedTokens } from \"./HighlightedTokens\";\n\nexport type CodeDiffView = \"unified\" | \"split\";\n\n// Rows = stacked/unified; columns = side-by-side/split.\nconst DIFF_VIEW_OPTIONS: SegmentedOption<CodeDiffView>[] = [\n { id: \"unified\", label: \"Unified\", icon: UiRows, title: \"Unified view\" },\n { id: \"split\", label: \"Split\", icon: UiColumns, title: \"Split view\" },\n];\n\n// Either two blobs (diffed in-repo) or a ready unified-diff string (what\n// `GitDiffPanel` already receives). `never` on the absent keys keeps the shapes\n// mutually exclusive.\ntype CodeDiffInput =\n | { original: string; modified: string; unified?: never }\n | { unified: string; original?: never; modified?: never };\n\nexport type CodeDiffProps = CodeDiffInput & {\n /** Highlighter language hint, e.g. `typescript`, `go`, `python`. */\n language?: string | undefined;\n /** Unified (single column) or split (side-by-side). Defaults to `unified`. */\n view?: CodeDiffView | undefined;\n /** Show old/new line-number gutters. Defaults to `true`. */\n showLineNumbers?: boolean | undefined;\n /** Render only the rows — no border, background, or language header. */\n bare?: boolean | undefined;\n className?: string | undefined;\n};\n\ntype ResolvedLine = {\n line: DiffLine;\n oldTokens: HighlightedLine | undefined;\n newTokens: HighlightedLine | undefined;\n};\n\ntype ResolvedHunk = { header: string | undefined; path: string | undefined; lines: ResolvedLine[] };\n\n// Row background tint — theme-aware via `[[data-theme=dark]_&]` (plain `dark:`\n// compiles to prefers-color-scheme in the built dist and ignores `data-theme`).\nconst ROW_TINT: Record<DiffLineType, string | undefined> = {\n context: undefined,\n add: \"bg-emerald-500/10\",\n remove: \"bg-rose-500/10\",\n};\n\nconst MARKER_TONE: Record<DiffLineType, string> = {\n context: \"text-muted-foreground\",\n add: \"text-emerald-700 [[data-theme=dark]_&]:text-emerald-300\",\n remove: \"text-rose-700 [[data-theme=dark]_&]:text-rose-300\",\n};\n\nconst MARKER: Record<DiffLineType, string> = { context: \"\", add: \"+\", remove: \"-\" };\n\nexport function CodeDiff(props: CodeDiffProps) {\n const { language, view: viewProp = \"unified\", showLineNumbers = true, bare = false, className } = props;\n const { unified, original, modified } = props;\n\n // `view` prop seeds the default; the header toggle then owns it.\n const [view, setView] = useState<CodeDiffView>(viewProp);\n\n const hunks = useMemo<DiffHunk[]>(\n () =>\n unified !== undefined\n ? parseUnifiedDiff(unified)\n : computeLineDiff(original ?? \"\", modified ?? \"\"),\n [unified, original, modified],\n );\n\n const { oldSource, newSource } = useMemo(() => buildSides(hunks), [hunks]);\n\n // Raw first paint, highlighted tokens swap in — matches CodeBlock. `null`\n // means \"not highlighted\" (empty/unsupported/loading); rows fall back to plain\n // text token-by-token.\n const [tokens, setTokens] = useState<{\n old: HighlightedLine[] | null;\n mod: HighlightedLine[] | null;\n }>({ old: null, mod: null });\n\n useEffect(() => {\n if (!language) {\n setTokens({ old: null, mod: null });\n return;\n }\n let cancelled = false;\n Promise.all([\n highlightToLines(oldSource, { lang: language }),\n highlightToLines(newSource, { lang: language }),\n ]).then(([old, mod]) => {\n if (!cancelled) setTokens({ old, mod });\n });\n return () => {\n cancelled = true;\n };\n }, [language, oldSource, newSource]);\n\n const resolved = useMemo(\n () => resolveHunks(hunks, tokens.old, tokens.mod),\n [hunks, tokens],\n );\n\n // Only a diff that spans more than one file labels its hunks with the path —\n // a single-file diff's path is already known from its surrounding context.\n const multiFile =\n new Set(resolved.map((hunk) => hunk.path).filter((p) => p !== undefined)).size > 1;\n\n let lastPath: string | undefined;\n const rows = resolved.map((hunk, index) => {\n const filePath =\n multiFile && hunk.path !== undefined && hunk.path !== lastPath ? hunk.path : undefined;\n lastPath = hunk.path;\n return (\n <Fragment key={index}>\n {filePath && <FileHeader path={filePath} />}\n {view === \"split\" ? (\n <SplitHunk hunk={hunk} showLineNumbers={showLineNumbers} />\n ) : (\n <UnifiedHunk hunk={hunk} showLineNumbers={showLineNumbers} />\n )}\n </Fragment>\n );\n });\n\n if (bare) {\n return (\n <div className={cn(\"overflow-x-auto font-mono text-xs leading-relaxed\", className)}>\n {rows}\n </div>\n );\n }\n return (\n <div className={cn(\"overflow-hidden rounded-md border border-border bg-muted/40\", className)}>\n <div className=\"flex items-center justify-between gap-2 border-b border-border px-3 py-1.5\">\n <span className=\"text-[11px] uppercase tracking-wide text-muted-foreground\">\n {language || \"diff\"}\n </span>\n <SegmentedControl\n size=\"sm\"\n aria-label=\"Diff view\"\n value={view}\n onChange={setView}\n options={DIFF_VIEW_OPTIONS}\n />\n </div>\n <div className=\"overflow-x-auto font-mono text-xs leading-relaxed\">{rows}</div>\n </div>\n );\n}\n\n// Reconstruct the full old side (context + remove) and new side (context + add)\n// as text — highlighting each side as a coherent blob so grammar state is intact.\nfunction buildSides(hunks: DiffHunk[]): { oldSource: string; newSource: string } {\n const oldLines: string[] = [];\n const newLines: string[] = [];\n for (const hunk of hunks) {\n for (const line of hunk.lines) {\n if (line.type !== \"add\") oldLines.push(line.content);\n if (line.type !== \"remove\") newLines.push(line.content);\n }\n }\n return { oldSource: oldLines.join(\"\\n\"), newSource: newLines.join(\"\\n\") };\n}\n\n// Map each diff line to its highlighted tokens via a sequential cursor per side\n// (not absolute line number, so unified-diff gaps between hunks stay aligned).\nfunction resolveHunks(\n hunks: DiffHunk[],\n oldTokens: HighlightedLine[] | null,\n newTokens: HighlightedLine[] | null,\n): ResolvedHunk[] {\n let oldCursor = 0;\n let newCursor = 0;\n return hunks.map((hunk) => ({\n header: hunk.header,\n path: hunk.path,\n lines: hunk.lines.map((line) => {\n const resolved: ResolvedLine = {\n line,\n oldTokens: line.type === \"add\" ? undefined : oldTokens?.[oldCursor],\n newTokens: line.type === \"remove\" ? undefined : newTokens?.[newCursor],\n };\n if (line.type !== \"add\") oldCursor += 1;\n if (line.type !== \"remove\") newCursor += 1;\n return resolved;\n }),\n }));\n}\n\nfunction UnifiedHunk({ hunk, showLineNumbers }: { hunk: ResolvedHunk; showLineNumbers: boolean }) {\n return (\n <>\n {hunk.header && <HunkHeader header={hunk.header} />}\n {hunk.lines.map((resolved, index) => {\n const { line } = resolved;\n const tokens = line.type === \"remove\" ? resolved.oldTokens : resolved.newTokens;\n return (\n <div key={index} className={cn(\"flex\", ROW_TINT[line.type])} data-diff-line={line.type}>\n {showLineNumbers && (\n <>\n <Gutter value={line.oldNumber} />\n <Gutter value={line.newNumber} />\n </>\n )}\n <Marker type={line.type} />\n <code className=\"whitespace-pre pl-1 pr-3\">\n <HighlightedTokens tokens={tokens} content={line.content} />\n </code>\n </div>\n );\n })}\n </>\n );\n}\n\nfunction SplitHunk({ hunk, showLineNumbers }: { hunk: ResolvedHunk; showLineNumbers: boolean }) {\n const pairs = useMemo(() => pairLines(hunk.lines), [hunk.lines]);\n return (\n <>\n {hunk.header && <HunkHeader header={hunk.header} />}\n {pairs.map((pair, index) => (\n <div key={index} className=\"flex\">\n <SplitCell resolved={pair.left} side=\"old\" showLineNumbers={showLineNumbers} />\n <span aria-hidden className=\"w-px shrink-0 bg-border\" />\n <SplitCell resolved={pair.right} side=\"new\" showLineNumbers={showLineNumbers} />\n </div>\n ))}\n </>\n );\n}\n\ntype SplitPair = { left: ResolvedLine | undefined; right: ResolvedLine | undefined };\n\n// Pair consecutive remove/add runs (remove[i] ↔ add[i]); leftovers get a blank\n// opposite cell. Context is one aligned row on both sides.\nfunction pairLines(lines: ResolvedLine[]): SplitPair[] {\n const pairs: SplitPair[] = [];\n let removes: ResolvedLine[] = [];\n let adds: ResolvedLine[] = [];\n const flush = () => {\n const max = Math.max(removes.length, adds.length);\n for (let i = 0; i < max; i++) pairs.push({ left: removes[i], right: adds[i] });\n removes = [];\n adds = [];\n };\n for (const resolved of lines) {\n if (resolved.line.type === \"remove\") removes.push(resolved);\n else if (resolved.line.type === \"add\") adds.push(resolved);\n else {\n flush();\n pairs.push({ left: resolved, right: resolved });\n }\n }\n flush();\n return pairs;\n}\n\nfunction SplitCell({\n resolved,\n side,\n showLineNumbers,\n}: {\n resolved: ResolvedLine | undefined;\n side: \"old\" | \"new\";\n showLineNumbers: boolean;\n}) {\n if (!resolved) return <div className=\"min-w-0 flex-1\" />;\n const { line } = resolved;\n const active =\n (side === \"old\" && line.type === \"remove\") || (side === \"new\" && line.type === \"add\");\n const type: DiffLineType = active ? line.type : \"context\";\n const number = side === \"old\" ? line.oldNumber : line.newNumber;\n const tokens = side === \"old\" ? resolved.oldTokens : resolved.newTokens;\n return (\n <div className={cn(\"flex min-w-0 flex-1\", active && ROW_TINT[line.type])} data-diff-line={type}>\n {showLineNumbers && <Gutter value={number} />}\n <Marker type={type} />\n <code className=\"min-w-0 whitespace-pre pl-1 pr-3\">\n <HighlightedTokens tokens={tokens} content={line.content} />\n </code>\n </div>\n );\n}\n\nfunction FileHeader({ path }: { path: string }) {\n return (\n <div\n data-diff-file={path}\n className=\"flex items-center gap-1.5 border-b border-border bg-muted px-2 py-1 font-medium text-foreground\"\n >\n <UiFile className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{path}</span>\n </div>\n );\n}\n\nfunction HunkHeader({ header }: { header: string }) {\n return (\n <div className=\"select-none bg-muted/60 px-2 py-0.5 text-muted-foreground\">{header}</div>\n );\n}\n\nfunction Gutter({ value }: { value: number | undefined }) {\n return (\n <span className=\"w-10 shrink-0 select-none px-2 text-right tabular-nums text-muted-foreground\">\n {value ?? \"\"}\n </span>\n );\n}\n\nfunction Marker({ type }: { type: DiffLineType }) {\n return (\n <span className={cn(\"w-4 shrink-0 select-none text-center\", MARKER_TONE[type])}>\n {MARKER[type]}\n </span>\n );\n}\n"],"names":["Fragment"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CodeDiff.js","sources":["../../src/data/CodeDiff.tsx"],"sourcesContent":["import { Fragment, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { SegmentedControl, type SegmentedOption } from \"../components/SegmentedControl\";\nimport { UiAdd, UiColumns, UiFile, UiRows } from \"../icons\";\nimport {\n computeLineDiff,\n parseUnifiedDiff,\n type DiffHunk,\n type DiffLine,\n type DiffLineType,\n} from \"./code-diff\";\nimport { highlightToLines, type HighlightedLine } from \"./code-highlight\";\nimport { HighlightedTokens } from \"./HighlightedTokens\";\n\nexport type CodeDiffView = \"unified\" | \"split\";\n\n// Rows = stacked/unified; columns = side-by-side/split.\nconst DIFF_VIEW_OPTIONS: SegmentedOption<CodeDiffView>[] = [\n { id: \"unified\", label: \"Unified\", icon: UiRows, title: \"Unified view\" },\n { id: \"split\", label: \"Split\", icon: UiColumns, title: \"Split view\" },\n];\n\n// Either two blobs (diffed in-repo) or a ready unified-diff string (what\n// `GitDiffPanel` already receives). `never` on the absent keys keeps the shapes\n// mutually exclusive.\ntype CodeDiffInput =\n | { original: string; modified: string; unified?: never }\n | { unified: string; original?: never; modified?: never };\n\nexport type DiffSide = \"old\" | \"new\";\n\n/** The diff line a gutter \"+\" button was clicked on. */\nexport interface DiffLineTarget {\n side: DiffSide;\n line: number;\n content: string;\n}\n\n/** A node rendered as a full-width row directly below the row at `(side, line)`. */\nexport interface DiffLineWidget {\n key: string;\n side: DiffSide;\n line: number;\n node: ReactNode;\n}\n\ntype LineAnnotations = {\n onLineAction: ((target: DiffLineTarget) => void) | undefined;\n lineWidgets: DiffLineWidget[] | undefined;\n};\n\nexport type CodeDiffProps = CodeDiffInput & {\n /**\n * When set, every numbered line shows a \"+\" button in its gutter (on row hover\n * or keyboard focus). Added/context lines report side `new`, deleted lines\n * `old`; in split view the left column is `old` and the right `new`.\n */\n onLineAction?: ((target: DiffLineTarget) => void) | undefined;\n /**\n * Rendered as a full-width row directly below the row matching `(side, line)`,\n * in array order. A context line matches both its `old` and `new` number.\n */\n lineWidgets?: DiffLineWidget[] | undefined;\n /** Highlighter language hint, e.g. `typescript`, `go`, `python`. */\n language?: string | undefined;\n /** Unified (single column) or split (side-by-side). Defaults to `unified`. */\n view?: CodeDiffView | undefined;\n /** Show old/new line-number gutters. Defaults to `true`. */\n showLineNumbers?: boolean | undefined;\n /** Render only the rows — no border, background, or language header. */\n bare?: boolean | undefined;\n className?: string | undefined;\n};\n\ntype ResolvedLine = {\n line: DiffLine;\n oldTokens: HighlightedLine | undefined;\n newTokens: HighlightedLine | undefined;\n};\n\ntype ResolvedHunk = { header: string | undefined; path: string | undefined; lines: ResolvedLine[] };\n\n// Row background tint — theme-aware via `[[data-theme=dark]_&]` (plain `dark:`\n// compiles to prefers-color-scheme in the built dist and ignores `data-theme`).\nconst ROW_TINT: Record<DiffLineType, string | undefined> = {\n context: undefined,\n add: \"bg-emerald-500/10\",\n remove: \"bg-rose-500/10\",\n};\n\nconst MARKER_TONE: Record<DiffLineType, string> = {\n context: \"text-muted-foreground\",\n add: \"text-emerald-700 [[data-theme=dark]_&]:text-emerald-300\",\n remove: \"text-rose-700 [[data-theme=dark]_&]:text-rose-300\",\n};\n\nconst MARKER: Record<DiffLineType, string> = { context: \"\", add: \"+\", remove: \"-\" };\n\nexport function CodeDiff(props: CodeDiffProps) {\n const { language, view: viewProp = \"unified\", showLineNumbers = true, bare = false, className } = props;\n const { unified, original, modified, onLineAction, lineWidgets } = props;\n const annotations: LineAnnotations = { onLineAction, lineWidgets };\n\n // `view` prop seeds the default; the header toggle then owns it.\n const [view, setView] = useState<CodeDiffView>(viewProp);\n\n const hunks = useMemo<DiffHunk[]>(\n () =>\n unified !== undefined\n ? parseUnifiedDiff(unified)\n : computeLineDiff(original ?? \"\", modified ?? \"\"),\n [unified, original, modified],\n );\n\n const { oldSource, newSource } = useMemo(() => buildSides(hunks), [hunks]);\n\n // Raw first paint, highlighted tokens swap in — matches CodeBlock. `null`\n // means \"not highlighted\" (empty/unsupported/loading); rows fall back to plain\n // text token-by-token.\n const [tokens, setTokens] = useState<{\n old: HighlightedLine[] | null;\n mod: HighlightedLine[] | null;\n }>({ old: null, mod: null });\n\n useEffect(() => {\n if (!language) {\n setTokens({ old: null, mod: null });\n return;\n }\n let cancelled = false;\n Promise.all([\n highlightToLines(oldSource, { lang: language }),\n highlightToLines(newSource, { lang: language }),\n ]).then(([old, mod]) => {\n if (!cancelled) setTokens({ old, mod });\n });\n return () => {\n cancelled = true;\n };\n }, [language, oldSource, newSource]);\n\n const resolved = useMemo(\n () => resolveHunks(hunks, tokens.old, tokens.mod),\n [hunks, tokens],\n );\n\n // Only a diff that spans more than one file labels its hunks with the path —\n // a single-file diff's path is already known from its surrounding context.\n const multiFile =\n new Set(resolved.map((hunk) => hunk.path).filter((p) => p !== undefined)).size > 1;\n\n let lastPath: string | undefined;\n const rows = resolved.map((hunk, index) => {\n const filePath =\n multiFile && hunk.path !== undefined && hunk.path !== lastPath ? hunk.path : undefined;\n lastPath = hunk.path;\n return (\n <Fragment key={index}>\n {filePath && <FileHeader path={filePath} />}\n {view === \"split\" ? (\n <SplitHunk hunk={hunk} showLineNumbers={showLineNumbers} annotations={annotations} />\n ) : (\n <UnifiedHunk hunk={hunk} showLineNumbers={showLineNumbers} annotations={annotations} />\n )}\n </Fragment>\n );\n });\n\n if (bare) {\n return (\n <div className={cn(\"overflow-x-auto font-mono text-xs leading-relaxed\", className)}>\n {rows}\n </div>\n );\n }\n return (\n <div className={cn(\"overflow-hidden rounded-md border border-border bg-muted/40\", className)}>\n <div className=\"flex items-center justify-between gap-2 border-b border-border px-3 py-1.5\">\n <span className=\"text-[11px] uppercase tracking-wide text-muted-foreground\">\n {language || \"diff\"}\n </span>\n <SegmentedControl\n size=\"sm\"\n aria-label=\"Diff view\"\n value={view}\n onChange={setView}\n options={DIFF_VIEW_OPTIONS}\n />\n </div>\n <div className=\"overflow-x-auto font-mono text-xs leading-relaxed\">{rows}</div>\n </div>\n );\n}\n\n// Reconstruct the full old side (context + remove) and new side (context + add)\n// as text — highlighting each side as a coherent blob so grammar state is intact.\nfunction buildSides(hunks: DiffHunk[]): { oldSource: string; newSource: string } {\n const oldLines: string[] = [];\n const newLines: string[] = [];\n for (const hunk of hunks) {\n for (const line of hunk.lines) {\n if (line.type !== \"add\") oldLines.push(line.content);\n if (line.type !== \"remove\") newLines.push(line.content);\n }\n }\n return { oldSource: oldLines.join(\"\\n\"), newSource: newLines.join(\"\\n\") };\n}\n\n// Map each diff line to its highlighted tokens via a sequential cursor per side\n// (not absolute line number, so unified-diff gaps between hunks stay aligned).\nfunction resolveHunks(\n hunks: DiffHunk[],\n oldTokens: HighlightedLine[] | null,\n newTokens: HighlightedLine[] | null,\n): ResolvedHunk[] {\n let oldCursor = 0;\n let newCursor = 0;\n return hunks.map((hunk) => ({\n header: hunk.header,\n path: hunk.path,\n lines: hunk.lines.map((line) => {\n const resolved: ResolvedLine = {\n line,\n oldTokens: line.type === \"add\" ? undefined : oldTokens?.[oldCursor],\n newTokens: line.type === \"remove\" ? undefined : newTokens?.[newCursor],\n };\n if (line.type !== \"add\") oldCursor += 1;\n if (line.type !== \"remove\") newCursor += 1;\n return resolved;\n }),\n }));\n}\n\ntype HunkProps = {\n hunk: ResolvedHunk;\n showLineNumbers: boolean;\n annotations: LineAnnotations;\n};\n\n// Which side of `line` a widget at (side, number) attaches to: context lines\n// carry both numbers, add lines only `new`, remove lines only `old`.\nfunction lineHasPosition(line: DiffLine, side: DiffSide, number: number): boolean {\n if (side === \"old\") return line.type !== \"add\" && line.oldNumber === number;\n return line.type !== \"remove\" && line.newNumber === number;\n}\n\nfunction WidgetRows({ widgets }: { widgets: DiffLineWidget[] }) {\n return (\n <>\n {widgets.map((widget) => (\n <div\n key={widget.key}\n data-diff-widget={widget.key}\n className=\"whitespace-normal border-y border-border bg-background px-3 py-2 font-sans text-sm leading-normal text-foreground\"\n >\n {widget.node}\n </div>\n ))}\n </>\n );\n}\n\nfunction UnifiedHunk({ hunk, showLineNumbers, annotations }: HunkProps) {\n const { onLineAction, lineWidgets } = annotations;\n return (\n <>\n {hunk.header && <HunkHeader header={hunk.header} />}\n {hunk.lines.map((resolved, index) => {\n const { line } = resolved;\n const tokens = line.type === \"remove\" ? resolved.oldTokens : resolved.newTokens;\n const actionSide: DiffSide = line.type === \"remove\" ? \"old\" : \"new\";\n const widgets = lineWidgets?.filter((w) => lineHasPosition(line, w.side, w.line));\n return (\n <Fragment key={index}>\n <div className={cn(\"group flex\", ROW_TINT[line.type])} data-diff-line={line.type}>\n {showLineNumbers && (\n <>\n <Gutter\n value={line.oldNumber}\n line={line}\n side=\"old\"\n actionSide={actionSide}\n onLineAction={onLineAction}\n />\n <Gutter\n value={line.newNumber}\n line={line}\n side=\"new\"\n actionSide={actionSide}\n onLineAction={onLineAction}\n />\n </>\n )}\n <Marker type={line.type} />\n <code className=\"whitespace-pre pl-1 pr-3\">\n <HighlightedTokens tokens={tokens} content={line.content} />\n </code>\n </div>\n {widgets && <WidgetRows widgets={widgets} />}\n </Fragment>\n );\n })}\n </>\n );\n}\n\nfunction SplitHunk({ hunk, showLineNumbers, annotations }: HunkProps) {\n const pairs = useMemo(() => pairLines(hunk.lines), [hunk.lines]);\n const { onLineAction, lineWidgets } = annotations;\n return (\n <>\n {hunk.header && <HunkHeader header={hunk.header} />}\n {pairs.map((pair, index) => {\n const widgets = lineWidgets?.filter(\n (w) =>\n (pair.left !== undefined && lineHasPosition(pair.left.line, w.side, w.line) && w.side === \"old\") ||\n (pair.right !== undefined && lineHasPosition(pair.right.line, w.side, w.line) && w.side === \"new\"),\n );\n return (\n <Fragment key={index}>\n <div className=\"flex\">\n <SplitCell\n resolved={pair.left}\n side=\"old\"\n showLineNumbers={showLineNumbers}\n onLineAction={onLineAction}\n />\n <span aria-hidden className=\"w-px shrink-0 bg-border\" />\n <SplitCell\n resolved={pair.right}\n side=\"new\"\n showLineNumbers={showLineNumbers}\n onLineAction={onLineAction}\n />\n </div>\n {widgets && <WidgetRows widgets={widgets} />}\n </Fragment>\n );\n })}\n </>\n );\n}\n\ntype SplitPair = { left: ResolvedLine | undefined; right: ResolvedLine | undefined };\n\n// Pair consecutive remove/add runs (remove[i] ↔ add[i]); leftovers get a blank\n// opposite cell. Context is one aligned row on both sides.\nfunction pairLines(lines: ResolvedLine[]): SplitPair[] {\n const pairs: SplitPair[] = [];\n let removes: ResolvedLine[] = [];\n let adds: ResolvedLine[] = [];\n const flush = () => {\n const max = Math.max(removes.length, adds.length);\n for (let i = 0; i < max; i++) pairs.push({ left: removes[i], right: adds[i] });\n removes = [];\n adds = [];\n };\n for (const resolved of lines) {\n if (resolved.line.type === \"remove\") removes.push(resolved);\n else if (resolved.line.type === \"add\") adds.push(resolved);\n else {\n flush();\n pairs.push({ left: resolved, right: resolved });\n }\n }\n flush();\n return pairs;\n}\n\nfunction SplitCell({\n resolved,\n side,\n showLineNumbers,\n onLineAction,\n}: {\n resolved: ResolvedLine | undefined;\n side: DiffSide;\n showLineNumbers: boolean;\n onLineAction: LineAnnotations[\"onLineAction\"];\n}) {\n if (!resolved) return <div className=\"min-w-0 flex-1\" />;\n const { line } = resolved;\n const active =\n (side === \"old\" && line.type === \"remove\") || (side === \"new\" && line.type === \"add\");\n const type: DiffLineType = active ? line.type : \"context\";\n const number = side === \"old\" ? line.oldNumber : line.newNumber;\n const tokens = side === \"old\" ? resolved.oldTokens : resolved.newTokens;\n return (\n <div\n className={cn(\"group flex min-w-0 flex-1\", active && ROW_TINT[line.type])}\n data-diff-line={type}\n >\n {showLineNumbers && (\n <Gutter value={number} line={line} side={side} actionSide={side} onLineAction={onLineAction} />\n )}\n <Marker type={type} />\n <code className=\"min-w-0 whitespace-pre pl-1 pr-3\">\n <HighlightedTokens tokens={tokens} content={line.content} />\n </code>\n </div>\n );\n}\n\nfunction FileHeader({ path }: { path: string }) {\n return (\n <div\n data-diff-file={path}\n className=\"flex items-center gap-1.5 border-b border-border bg-muted px-2 py-1 font-medium text-foreground\"\n >\n <UiFile className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{path}</span>\n </div>\n );\n}\n\nfunction HunkHeader({ header }: { header: string }) {\n return (\n <div className=\"select-none bg-muted/60 px-2 py-0.5 text-muted-foreground\">{header}</div>\n );\n}\n\n// `side` is the number this gutter shows; the \"+\" button only renders in the\n// `actionSide` gutter so a unified row (two gutters) exposes exactly one.\nfunction Gutter({\n value,\n line,\n side,\n actionSide,\n onLineAction,\n}: {\n value: number | undefined;\n line: DiffLine;\n side: DiffSide;\n actionSide: DiffSide;\n onLineAction: LineAnnotations[\"onLineAction\"];\n}) {\n const showAction = onLineAction !== undefined && value !== undefined && side === actionSide;\n return (\n <span className=\"relative w-10 shrink-0 select-none px-2 text-right tabular-nums text-muted-foreground\">\n {value ?? \"\"}\n {showAction && (\n <button\n type=\"button\"\n aria-label={`Comment on line ${value}`}\n onClick={() => onLineAction({ side, line: value, content: line.content })}\n className=\"absolute inset-y-0 left-0.5 flex items-center rounded-sm bg-background text-primary opacity-0 hover:text-primary/80 focus-visible:opacity-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring group-hover:opacity-100\"\n >\n <UiAdd className=\"size-3.5\" />\n </button>\n )}\n </span>\n );\n}\n\nfunction Marker({ type }: { type: DiffLineType }) {\n return (\n <span className={cn(\"w-4 shrink-0 select-none text-center\", MARKER_TONE[type])}>\n {MARKER[type]}\n </span>\n );\n}\n"],"names":["Fragment"],"mappings":"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"}
|
|
@@ -7,7 +7,9 @@ const IconButton = require("../components/IconButton.cjs");
|
|
|
7
7
|
const graphColumnsLayout = require("./graph-columns-layout.cjs");
|
|
8
8
|
const graphLayout = require("./graph-layout.cjs");
|
|
9
9
|
const GraphDiagramEdges = require("./GraphDiagramEdges.cjs");
|
|
10
|
+
const GraphDiagramGroups = require("./GraphDiagramGroups.cjs");
|
|
10
11
|
const GraphDiagramNodes = require("./GraphDiagramNodes.cjs");
|
|
12
|
+
const useGroupWindows = require("./use-group-windows.cjs");
|
|
11
13
|
const graphDiagramModel = require("./graph-diagram-model.cjs");
|
|
12
14
|
const useEdgeFocus = require("./use-edge-focus.cjs");
|
|
13
15
|
const usePanZoom = require("./use-pan-zoom.cjs");
|
|
@@ -19,9 +21,23 @@ const UiHome = require("../icons/components/UiHome.cjs");
|
|
|
19
21
|
const DEFAULT_NODE_WIDTH = 168;
|
|
20
22
|
const DEFAULT_NODE_HEIGHT = 60;
|
|
21
23
|
const DEFAULT_COMPACT_NODE_HEIGHT = 24;
|
|
22
|
-
function compactHeights(nodes, layout, compactNodeHeight) {
|
|
24
|
+
function compactHeights(nodes, layout, compactNodeHeight, standIns, headerHeight) {
|
|
23
25
|
if (layout !== "columns") return {};
|
|
24
|
-
return Object.fromEntries(nodes.
|
|
26
|
+
return Object.fromEntries(nodes.flatMap((node) => {
|
|
27
|
+
if (standIns.has(node.id)) return [[node.id, headerHeight]];
|
|
28
|
+
return node.size === "compact" ? [[node.id, compactNodeHeight]] : [];
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
function collapsedStandIns(nodes, groups) {
|
|
32
|
+
const collapsed = new Set(groups == null ? void 0 : groups.filter((group) => group.collapsed === true).map((group) => group.id));
|
|
33
|
+
const standIns = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const node of nodes) {
|
|
35
|
+
if (node.group === void 0 || !collapsed.has(node.group)) continue;
|
|
36
|
+
const held = standIns.get(node.group);
|
|
37
|
+
if (held !== void 0) throw new Error(`GraphDiagram: collapsed group "${node.group}" holds "${held}" and "${node.id}"; it draws one stand-in`);
|
|
38
|
+
standIns.set(node.group, node.id);
|
|
39
|
+
}
|
|
40
|
+
return standIns;
|
|
25
41
|
}
|
|
26
42
|
function assertEdgesReferenceKnownNodes(nodes, edges) {
|
|
27
43
|
const ids = new Set(nodes.map((node) => node.id));
|
|
@@ -40,7 +56,7 @@ function requireLevel(node) {
|
|
|
40
56
|
}
|
|
41
57
|
return node.level;
|
|
42
58
|
}
|
|
43
|
-
function layoutNodes(nodes, edges, { layout, nodeWidth, nodeHeight, compactNodeHeight, columnGap, rowGap }) {
|
|
59
|
+
function layoutNodes(nodes, edges, { layout, nodeWidth, nodeHeight, compactNodeHeight, records, collapsed, recordHeaderHeight, columnGap, rowGap }) {
|
|
44
60
|
switch (layout) {
|
|
45
61
|
case "ring":
|
|
46
62
|
return { ...graphLayout.ringLayout(nodes, { nodeWidth, nodeHeight }), groups: [] };
|
|
@@ -57,6 +73,9 @@ function layoutNodes(nodes, edges, { layout, nodeWidth, nodeHeight, compactNodeH
|
|
|
57
73
|
nodeWidth,
|
|
58
74
|
nodeHeight,
|
|
59
75
|
compactNodeHeight,
|
|
76
|
+
records,
|
|
77
|
+
collapsed,
|
|
78
|
+
recordHeaderHeight,
|
|
60
79
|
...columnGap !== void 0 ? { columnGap } : {},
|
|
61
80
|
...rowGap !== void 0 ? { rowGap } : {}
|
|
62
81
|
}
|
|
@@ -99,9 +118,11 @@ function GraphDiagram({
|
|
|
99
118
|
onEdgeSelect,
|
|
100
119
|
edgeFocus = "auto",
|
|
101
120
|
groups,
|
|
121
|
+
onGroupToggle,
|
|
102
122
|
nodeWidth = DEFAULT_NODE_WIDTH,
|
|
103
123
|
nodeHeight = DEFAULT_NODE_HEIGHT,
|
|
104
124
|
compactNodeHeight = DEFAULT_COMPACT_NODE_HEIGHT,
|
|
125
|
+
recordHeaderHeight = 24,
|
|
105
126
|
columnGap,
|
|
106
127
|
rowGap,
|
|
107
128
|
zoomable = false,
|
|
@@ -114,22 +135,39 @@ function GraphDiagram({
|
|
|
114
135
|
}) {
|
|
115
136
|
assertEdgesReferenceKnownNodes(nodes, edges);
|
|
116
137
|
const markerPrefix = `graph-diagram-${react.useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
const records = react.useMemo(() => (groups == null ? void 0 : groups.filter((group) => group.variant === "record").map((group) => group.id)) ?? [], [groups]);
|
|
139
|
+
const recordNodes = react.useMemo(() => new Set(layout === "columns" ? nodes.filter((node) => node.group !== void 0 && records.includes(node.group)).map((node) => node.id) : []), [layout, nodes, records]);
|
|
140
|
+
const standIns = react.useMemo(() => layout === "columns" ? collapsedStandIns(nodes, groups) : /* @__PURE__ */ new Map(), [layout, nodes, groups]);
|
|
141
|
+
const collapsed = react.useMemo(() => [...standIns.keys()], [standIns]);
|
|
142
|
+
const { width, height, positions: laidOut, groups: groupBoxes } = react.useMemo(
|
|
143
|
+
() => layoutNodes(nodes, edges, { layout, nodeWidth, nodeHeight, compactNodeHeight, records, collapsed, recordHeaderHeight, columnGap, rowGap }),
|
|
144
|
+
[layout, nodes, edges, nodeWidth, nodeHeight, compactNodeHeight, records, collapsed, recordHeaderHeight, columnGap, rowGap]
|
|
120
145
|
);
|
|
121
|
-
const nodeHeights = react.useMemo(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
[edges, positions, nodeWidth, nodeHeight, nodeHeights, layout]
|
|
146
|
+
const nodeHeights = react.useMemo(
|
|
147
|
+
() => compactHeights(nodes, layout, compactNodeHeight, new Set(standIns.values()), recordHeaderHeight),
|
|
148
|
+
[nodes, layout, compactNodeHeight, standIns, recordHeaderHeight]
|
|
125
149
|
);
|
|
126
150
|
const panZoom = usePanZoom.usePanZoom({
|
|
127
151
|
enabled: zoomable,
|
|
128
152
|
wheel: wheelZoom,
|
|
129
153
|
contentWidth: width,
|
|
130
154
|
contentHeight: height,
|
|
131
|
-
opening: openingFit(
|
|
155
|
+
opening: openingFit(laidOut, fitMinScale, focusId)
|
|
132
156
|
});
|
|
157
|
+
const windows = useGroupWindows.useGroupWindows({ boxes: groupBoxes, positions: laidOut, reveal: selectedId ?? focusId, stageRef: panZoom.contentRef, scale: zoomable ? panZoom.transform.scale : 1 });
|
|
158
|
+
const { positions } = windows;
|
|
159
|
+
const routes = react.useMemo(
|
|
160
|
+
() => graphEdgeRouting.routeEdges(edges, positions, { nodeWidth, nodeHeight, nodeHeights }, { anchor: layout === "columns" ? "side" : "center" }),
|
|
161
|
+
[edges, positions, nodeWidth, nodeHeight, nodeHeights, layout]
|
|
162
|
+
);
|
|
163
|
+
const drawnNodes = react.useMemo(() => {
|
|
164
|
+
const standing = new Set(standIns.values());
|
|
165
|
+
return nodes.filter((node) => !windows.hidden.has(node.id) && !standing.has(node.id));
|
|
166
|
+
}, [nodes, windows.hidden, standIns]);
|
|
167
|
+
const windowOf = react.useMemo(
|
|
168
|
+
() => new Map(groupBoxes.flatMap((box) => box.window ? box.window.members.map((id) => [id, box.id]) : [])),
|
|
169
|
+
[groupBoxes]
|
|
170
|
+
);
|
|
133
171
|
const natural = layout === "columns" || zoomable;
|
|
134
172
|
const place = natural ? ({ x, y }) => ({ left: x, top: y }) : ({ x, y }) => ({ left: toPercent(x, width), top: toPercent(y, height) });
|
|
135
173
|
const focus = useEdgeFocus.useEdgeFocus({ mode: edgeFocus, edges, selectedId, selectedEdgeId });
|
|
@@ -155,7 +193,19 @@ function GraphDiagram({
|
|
|
155
193
|
className: utils.cn("relative", natural ? !zoomable && "mx-auto" : "w-full"),
|
|
156
194
|
style: stageStyle({ width, height }, natural, zoomable ? panZoom : void 0),
|
|
157
195
|
children: [
|
|
158
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
196
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
197
|
+
GraphDiagramGroups.GraphDiagramGroupBoxes,
|
|
198
|
+
{
|
|
199
|
+
boxes: groupBoxes,
|
|
200
|
+
groups,
|
|
201
|
+
starts: windows.starts,
|
|
202
|
+
onScroll: windows.scroll,
|
|
203
|
+
onGroupToggle,
|
|
204
|
+
standIns,
|
|
205
|
+
selectedId,
|
|
206
|
+
onNodeSelect
|
|
207
|
+
}
|
|
208
|
+
),
|
|
159
209
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
160
210
|
GraphDiagramEdges.GraphDiagramEdgePaths,
|
|
161
211
|
{
|
|
@@ -175,12 +225,14 @@ function GraphDiagram({
|
|
|
175
225
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
176
226
|
GraphDiagramNodes.GraphDiagramNodes,
|
|
177
227
|
{
|
|
178
|
-
nodes,
|
|
228
|
+
nodes: drawnNodes,
|
|
179
229
|
positions,
|
|
230
|
+
windowOf,
|
|
180
231
|
place,
|
|
181
232
|
nodeWidth,
|
|
182
233
|
nodeHeight,
|
|
183
234
|
nodeHeights,
|
|
235
|
+
recordNodes,
|
|
184
236
|
selectedId,
|
|
185
237
|
onNodeSelect,
|
|
186
238
|
onNodeExpand,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GraphDiagram.cjs","sources":["../../src/data/GraphDiagram.tsx"],"sourcesContent":["import { useId, useMemo, type CSSProperties } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { IconButton } from \"../components/IconButton\";\nimport { UiFullscreen, UiHome, UiZoomIn, UiZoomOut } from \"../icons\";\nimport { columnsLayout, type ColumnsLayoutResult } from \"./graph-columns-layout\";\nimport { ringLayout, routeEdges, type GraphLayoutPosition } from \"./graph-layout\";\nimport { GraphDiagramEdgeLabels, GraphDiagramEdgePaths } from \"./GraphDiagramEdges\";\nimport { GraphDiagramGroupBoxes, GraphDiagramNodes } from \"./GraphDiagramNodes\";\nimport {\n requireEntry,\n type GraphDiagramEdge,\n type GraphDiagramEdgeFocus,\n type GraphDiagramGroup,\n type GraphDiagramLayout,\n type GraphDiagramNode,\n type GraphPlacer,\n} from \"./graph-diagram-model\";\nimport { useEdgeFocus } from \"./use-edge-focus\";\nimport { usePanZoom, type FitOptions, type PanZoom, type WheelMode } from \"./use-pan-zoom\";\n\nexport type {\n GraphDiagramEdge,\n GraphDiagramEdgeFocus,\n GraphDiagramGroup,\n GraphDiagramLayout,\n GraphDiagramNode,\n} from \"./graph-diagram-model\";\n\n// GraphDiagram renders a generic, type-agnostic node/edge diagram. It knows\n// nothing about the domain: a producer maps its data into\n// GraphDiagramNode/GraphDiagramEdge and gets layout, edge routing and\n// selection behaviour.\n//\n// \"ring\" suits a small cycle of resources and processes — relationships with\n// back edges or reciprocal pairs. \"columns\" suits a directed graph with a\n// natural depth (a call graph, a dependency chain): nodes sit in the column of\n// their `level`, grouped and boxed by `group`.\n//\n// The \"columns\" layout, and any zoomable diagram, is drawn at its natural\n// pixel size so node boxes, edges and group boxes share one coordinate system\n// at every container width and zoom level. A non-zoomable \"ring\" keeps scaling\n// its node positions to the container width.\n\nexport interface GraphDiagramProps {\n nodes: GraphDiagramNode[];\n edges: GraphDiagramEdge[];\n /** Defaults to \"ring\". \"columns\" needs a `level` on every node. */\n layout?: GraphDiagramLayout;\n selectedId?: string;\n onNodeSelect?: (id: string) => void;\n /** Called by a node's `+N` control (see `expandCount`); does not select the node. */\n onNodeExpand?: (id: string) => void;\n selectedEdgeId?: string;\n /** Makes edges, and their label pills, selectable. */\n onEdgeSelect?: (id: string) => void;\n /**\n * Whether an edge shows its pill only while in focus, and fades while another is. Defaults to\n * \"auto\": a diagram with more than 32 edges is focused, a smaller one shows everything.\n */\n edgeFocus?: GraphDiagramEdgeFocus;\n /** \"columns\" layout: captions for the group rectangles. A group without an entry shows its id. */\n groups?: GraphDiagramGroup[];\n nodeWidth?: number;\n nodeHeight?: number;\n /**\n * \"columns\" layout: the height of a node with `size: \"compact\"`, drawn on one line. A group of only\n * compact nodes stacks them closer and pads them tighter. Defaults to 24.\n */\n compactNodeHeight?: number;\n /** \"columns\" layout: horizontal gap between columns in px. Leave room for edge label pills. */\n columnGap?: number;\n /** \"columns\" layout: vertical gap between stacked nodes in px. */\n rowGap?: number;\n /** Ctrl/meta + wheel (or pinch) zooms, dragging the background pans, and zoom controls appear. */\n zoomable?: boolean;\n /** Zoomable: \"zoom\" makes a plain mouse wheel zoom too, for a diagram that fills its pane. Defaults to \"modifier\". */\n wheelZoom?: WheelMode;\n /**\n * Zoomable: the smallest scale the diagram opens at, so its text stays readable. A diagram too\n * large for it opens overflowing, centred on `focusId`, and is panned to reach the rest. `Fit to\n * view` still shows everything, and `Reset view` returns here. Unset, the diagram opens whole.\n */\n fitMinScale?: number;\n /** Zoomable: the node an overflowing diagram opens centred on. Defaults to the diagram's centre. */\n focusId?: string;\n maxHeight?: number | string;\n ariaLabel: string;\n className?: string;\n}\n\nconst DEFAULT_NODE_WIDTH = 168;\nconst DEFAULT_NODE_HEIGHT = 60;\nconst DEFAULT_COMPACT_NODE_HEIGHT = 24;\n\ninterface LayoutOptions {\n layout: GraphDiagramLayout;\n nodeWidth: number;\n nodeHeight: number;\n compactNodeHeight: number;\n columnGap: number | undefined;\n rowGap: number | undefined;\n}\n\n/** The heights of the nodes not drawn at the node height: the compact ones, in the \"columns\" layout. */\nfunction compactHeights(nodes: GraphDiagramNode[], layout: GraphDiagramLayout, compactNodeHeight: number): Record<string, number> {\n if (layout !== \"columns\") return {};\n return Object.fromEntries(nodes.filter((node) => node.size === \"compact\").map((node) => [node.id, compactNodeHeight]));\n}\n\nfunction assertEdgesReferenceKnownNodes(nodes: GraphDiagramNode[], edges: GraphDiagramEdge[]): void {\n const ids = new Set(nodes.map((node) => node.id));\n for (const edge of edges) {\n if (!ids.has(edge.from)) {\n throw new Error(`GraphDiagram: edge \"${edge.id}\" references unknown node \"${edge.from}\"`);\n }\n if (!ids.has(edge.to)) {\n throw new Error(`GraphDiagram: edge \"${edge.id}\" references unknown node \"${edge.to}\"`);\n }\n }\n}\n\nfunction requireLevel(node: GraphDiagramNode): number {\n if (node.level === undefined || !Number.isFinite(node.level)) {\n throw new Error(`GraphDiagram: node \"${node.id}\" needs a finite level for the \"columns\" layout`);\n }\n return node.level;\n}\n\nfunction layoutNodes(\n nodes: GraphDiagramNode[],\n edges: GraphDiagramEdge[],\n { layout, nodeWidth, nodeHeight, compactNodeHeight, columnGap, rowGap }: LayoutOptions,\n): ColumnsLayoutResult {\n switch (layout) {\n case \"ring\":\n return { ...ringLayout(nodes, { nodeWidth, nodeHeight }), groups: [] };\n case \"columns\":\n return columnsLayout(\n nodes.map((node) => ({\n id: node.id,\n level: requireLevel(node),\n ...(node.group !== undefined ? { group: node.group } : {}),\n ...(node.size === \"compact\" ? { compact: true } : {}),\n })),\n edges,\n {\n nodeWidth,\n nodeHeight,\n compactNodeHeight,\n ...(columnGap !== undefined ? { columnGap } : {}),\n ...(rowGap !== undefined ? { rowGap } : {}),\n },\n );\n default:\n throw new Error(`GraphDiagram: unknown layout \"${layout satisfies never}\"`);\n }\n}\n\nfunction toPercent(value: number, extent: number): string {\n return `${(value / extent) * 100}%`;\n}\n\nfunction stageStyle(size: { width: number; height: number }, natural: boolean, panZoom: PanZoom | undefined): CSSProperties {\n if (!natural) return { aspectRatio: `${size.width} / ${size.height}` };\n if (!panZoom) return size;\n const { x, y, scale } = panZoom.transform;\n return { ...size, transform: `translate(${x}px, ${y}px) scale(${scale})`, transformOrigin: \"0 0\" };\n}\n\n/** `resettable`: the diagram opens at another view than the whole of it, so there is one to return to. */\nfunction ZoomControls({ panZoom, resettable }: { panZoom: PanZoom; resettable: boolean }) {\n return (\n <div className=\"absolute right-2 top-2 flex cursor-default gap-0.5 rounded-md border border-border bg-background p-0.5 shadow-sm\">\n <IconButton icon={UiZoomOut} label=\"Zoom out\" className=\"size-6\" onClick={panZoom.zoomOut} />\n <IconButton icon={UiZoomIn} label=\"Zoom in\" className=\"size-6\" onClick={panZoom.zoomIn} />\n <IconButton icon={UiFullscreen} label=\"Fit to view\" className=\"size-6\" onClick={panZoom.fit} />\n {resettable && <IconButton icon={UiHome} label=\"Reset view\" className=\"size-6\" onClick={panZoom.reset} />}\n </div>\n );\n}\n\n/** How a zoomable diagram opens: no smaller than `fitMinScale`, centred on the `focusId` node. */\nfunction openingFit(\n positions: Record<string, GraphLayoutPosition>,\n fitMinScale: number | undefined,\n focusId: string | undefined,\n): FitOptions {\n return {\n ...(fitMinScale !== undefined ? { minScale: fitMinScale } : {}),\n ...(focusId !== undefined ? { focus: requireEntry(positions, focusId, \"layout position\") } : {}),\n };\n}\n\nexport function GraphDiagram({\n nodes,\n edges,\n layout = \"ring\",\n selectedId,\n onNodeSelect,\n onNodeExpand,\n selectedEdgeId,\n onEdgeSelect,\n edgeFocus = \"auto\",\n groups,\n nodeWidth = DEFAULT_NODE_WIDTH,\n nodeHeight = DEFAULT_NODE_HEIGHT,\n compactNodeHeight = DEFAULT_COMPACT_NODE_HEIGHT,\n columnGap,\n rowGap,\n zoomable = false,\n wheelZoom = \"modifier\",\n fitMinScale,\n focusId,\n maxHeight,\n ariaLabel,\n className,\n}: GraphDiagramProps) {\n assertEdgesReferenceKnownNodes(nodes, edges);\n const markerPrefix = `graph-diagram-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n\n const { width, height, positions, groups: groupBoxes } = useMemo(\n () => layoutNodes(nodes, edges, { layout, nodeWidth, nodeHeight, compactNodeHeight, columnGap, rowGap }),\n [layout, nodes, edges, nodeWidth, nodeHeight, compactNodeHeight, columnGap, rowGap],\n );\n const nodeHeights = useMemo(() => compactHeights(nodes, layout, compactNodeHeight), [nodes, layout, compactNodeHeight]);\n const routes = useMemo(\n () =>\n routeEdges(edges, positions, { nodeWidth, nodeHeight, nodeHeights }, { anchor: layout === \"columns\" ? \"side\" : \"center\" }),\n [edges, positions, nodeWidth, nodeHeight, nodeHeights, layout],\n );\n\n const panZoom = usePanZoom({\n enabled: zoomable,\n wheel: wheelZoom,\n contentWidth: width,\n contentHeight: height,\n opening: openingFit(positions, fitMinScale, focusId),\n });\n const natural = layout === \"columns\" || zoomable;\n const place: GraphPlacer = natural\n ? ({ x, y }) => ({ left: x, top: y })\n : ({ x, y }) => ({ left: toPercent(x, width), top: toPercent(y, height) });\n const focus = useEdgeFocus({ mode: edgeFocus, edges, selectedId, selectedEdgeId });\n const edgeSelection = { selectedEdgeId, onEdgeSelect, focus: focus.state };\n\n return (\n <div\n ref={panZoom.viewportRef}\n className={cn(\n \"w-full\",\n zoomable ? \"relative touch-none overflow-hidden\" : \"overflow-auto\",\n zoomable && (panZoom.panning ? \"cursor-grabbing select-none\" : \"cursor-grab\"),\n className,\n )}\n style={maxHeight != null ? { maxHeight } : undefined}\n {...panZoom.handlers}\n >\n <div\n ref={panZoom.contentRef}\n data-graph-stage=\"\"\n className={cn(\"relative\", natural ? !zoomable && \"mx-auto\" : \"w-full\")}\n style={stageStyle({ width, height }, natural, zoomable ? panZoom : undefined)}\n >\n <GraphDiagramGroupBoxes boxes={groupBoxes} groups={groups} />\n <GraphDiagramEdgePaths\n edges={edges}\n nodes={nodes}\n routes={routes}\n width={width}\n height={height}\n ariaLabel={ariaLabel}\n markerPrefix={markerPrefix}\n onEdgeHover={focus.onEdgeHover}\n {...edgeSelection}\n />\n <div className=\"pointer-events-none absolute inset-0\">\n <GraphDiagramEdgeLabels edges={edges} routes={routes} place={place} spread={natural} {...edgeSelection} />\n <GraphDiagramNodes\n nodes={nodes}\n positions={positions}\n place={place}\n nodeWidth={nodeWidth}\n nodeHeight={nodeHeight}\n nodeHeights={nodeHeights}\n selectedId={selectedId}\n onNodeSelect={onNodeSelect}\n onNodeExpand={onNodeExpand}\n onNodeHover={focus.onNodeHover}\n />\n </div>\n </div>\n {zoomable && <ZoomControls panZoom={panZoom} resettable={fitMinScale !== undefined} />}\n </div>\n );\n}\n"],"names":["ringLayout","columnsLayout","jsxs","jsx","IconButton","UiZoomOut","UiZoomIn","UiFullscreen","UiHome","requireEntry","useId","useMemo","routeEdges","usePanZoom","useEdgeFocus","cn","GraphDiagramGroupBoxes","GraphDiagramEdgePaths","GraphDiagramEdgeLabels","GraphDiagramNodes"],"mappings":";;;;;;;;;;;;;;;;;;AA0FA,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,8BAA8B;AAYpC,SAAS,eAAe,OAA2B,QAA4B,mBAAmD;AAChI,MAAI,WAAW,UAAW,QAAO,CAAA;AACjC,SAAO,OAAO,YAAY,MAAM,OAAO,CAAC,SAAS,KAAK,SAAS,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,iBAAiB,CAAC,CAAC;AACvH;AAEA,SAAS,+BAA+B,OAA2B,OAAiC;AAClG,QAAM,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAChD,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,IAAI,IAAI,KAAK,IAAI,GAAG;AACvB,YAAM,IAAI,MAAM,uBAAuB,KAAK,EAAE,8BAA8B,KAAK,IAAI,GAAG;AAAA,IAC1F;AACA,QAAI,CAAC,IAAI,IAAI,KAAK,EAAE,GAAG;AACrB,YAAM,IAAI,MAAM,uBAAuB,KAAK,EAAE,8BAA8B,KAAK,EAAE,GAAG;AAAA,IACxF;AAAA,EACF;AACF;AAEA,SAAS,aAAa,MAAgC;AACpD,MAAI,KAAK,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,KAAK,GAAG;AAC5D,UAAM,IAAI,MAAM,uBAAuB,KAAK,EAAE,iDAAiD;AAAA,EACjG;AACA,SAAO,KAAK;AACd;AAEA,SAAS,YACP,OACA,OACA,EAAE,QAAQ,WAAW,YAAY,mBAAmB,WAAW,UAC1C;AACrB,UAAQ,QAAA;AAAA,IACN,KAAK;AACH,aAAO,EAAE,GAAGA,YAAAA,WAAW,OAAO,EAAE,WAAW,WAAA,CAAY,GAAG,QAAQ,GAAC;AAAA,IACrE,KAAK;AACH,aAAOC,mBAAAA;AAAAA,QACL,MAAM,IAAI,CAAC,UAAU;AAAA,UACnB,IAAI,KAAK;AAAA,UACT,OAAO,aAAa,IAAI;AAAA,UACxB,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,KAAK,MAAA,IAAU,CAAA;AAAA,UACvD,GAAI,KAAK,SAAS,YAAY,EAAE,SAAS,KAAA,IAAS,CAAA;AAAA,QAAC,EACnD;AAAA,QACF;AAAA,QACA;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,UACA,GAAI,cAAc,SAAY,EAAE,UAAA,IAAc,CAAA;AAAA,UAC9C,GAAI,WAAW,SAAY,EAAE,WAAW,CAAA;AAAA,QAAC;AAAA,MAC3C;AAAA,IAEJ;AACE,YAAM,IAAI,MAAM,iCAAiC,MAAsB,GAAG;AAAA,EAAA;AAEhF;AAEA,SAAS,UAAU,OAAe,QAAwB;AACxD,SAAO,GAAI,QAAQ,SAAU,GAAG;AAClC;AAEA,SAAS,WAAW,MAAyC,SAAkB,SAA6C;AAC1H,MAAI,CAAC,QAAS,QAAO,EAAE,aAAa,GAAG,KAAK,KAAK,MAAM,KAAK,MAAM,GAAA;AAClE,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,EAAE,GAAG,GAAG,MAAA,IAAU,QAAQ;AAChC,SAAO,EAAE,GAAG,MAAM,WAAW,aAAa,CAAC,OAAO,CAAC,aAAa,KAAK,KAAK,iBAAiB,MAAA;AAC7F;AAGA,SAAS,aAAa,EAAE,SAAS,cAAyD;AACxF,SACEC,2BAAAA,KAAC,OAAA,EAAI,WAAU,oHACb,UAAA;AAAA,IAAAC,2BAAAA,IAACC,WAAAA,YAAA,EAAW,MAAMC,UAAAA,WAAW,OAAM,YAAW,WAAU,UAAS,SAAS,QAAQ,QAAA,CAAS;AAAA,IAC3FF,2BAAAA,IAACC,WAAAA,YAAA,EAAW,MAAME,SAAAA,UAAU,OAAM,WAAU,WAAU,UAAS,SAAS,QAAQ,OAAA,CAAQ;AAAA,IACxFH,2BAAAA,IAACC,WAAAA,YAAA,EAAW,MAAMG,aAAAA,cAAc,OAAM,eAAc,WAAU,UAAS,SAAS,QAAQ,IAAA,CAAK;AAAA,IAC5F,cAAcJ,2BAAAA,IAACC,WAAAA,YAAA,EAAW,MAAMI,OAAAA,QAAQ,OAAM,cAAa,WAAU,UAAS,SAAS,QAAQ,MAAA,CAAO;AAAA,EAAA,GACzG;AAEJ;AAGA,SAAS,WACP,WACA,aACA,SACY;AACZ,SAAO;AAAA,IACL,GAAI,gBAAgB,SAAY,EAAE,UAAU,YAAA,IAAgB,CAAA;AAAA,IAC5D,GAAI,YAAY,SAAY,EAAE,OAAOC,kBAAAA,aAAa,WAAW,SAAS,iBAAiB,MAAM,CAAA;AAAA,EAAC;AAElG;AAEO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsB;AACpB,iCAA+B,OAAO,KAAK;AAC3C,QAAM,eAAe,iBAAiBC,MAAAA,MAAA,EAAQ,QAAQ,mBAAmB,EAAE,CAAC;AAE5E,QAAM,EAAE,OAAO,QAAQ,WAAW,QAAQ,eAAeC,MAAAA;AAAAA,IACvD,MAAM,YAAY,OAAO,OAAO,EAAE,QAAQ,WAAW,YAAY,mBAAmB,WAAW,QAAQ;AAAA,IACvG,CAAC,QAAQ,OAAO,OAAO,WAAW,YAAY,mBAAmB,WAAW,MAAM;AAAA,EAAA;AAEpF,QAAM,cAAcA,MAAAA,QAAQ,MAAM,eAAe,OAAO,QAAQ,iBAAiB,GAAG,CAAC,OAAO,QAAQ,iBAAiB,CAAC;AACtH,QAAM,SAASA,MAAAA;AAAAA,IACb,MACEC,iBAAAA,WAAW,OAAO,WAAW,EAAE,WAAW,YAAY,eAAe,EAAE,QAAQ,WAAW,YAAY,SAAS,UAAU;AAAA,IAC3H,CAAC,OAAO,WAAW,WAAW,YAAY,aAAa,MAAM;AAAA,EAAA;AAG/D,QAAM,UAAUC,WAAAA,WAAW;AAAA,IACzB,SAAS;AAAA,IACT,OAAO;AAAA,IACP,cAAc;AAAA,IACd,eAAe;AAAA,IACf,SAAS,WAAW,WAAW,aAAa,OAAO;AAAA,EAAA,CACpD;AACD,QAAM,UAAU,WAAW,aAAa;AACxC,QAAM,QAAqB,UACvB,CAAC,EAAE,GAAG,EAAA,OAAS,EAAE,MAAM,GAAG,KAAK,EAAA,KAC/B,CAAC,EAAE,GAAG,EAAA,OAAS,EAAE,MAAM,UAAU,GAAG,KAAK,GAAG,KAAK,UAAU,GAAG,MAAM,EAAA;AACxE,QAAM,QAAQC,aAAAA,aAAa,EAAE,MAAM,WAAW,OAAO,YAAY,gBAAgB;AACjF,QAAM,gBAAgB,EAAE,gBAAgB,cAAc,OAAO,MAAM,MAAA;AAEnE,SACEZ,2BAAAA;AAAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK,QAAQ;AAAA,MACb,WAAWa,MAAAA;AAAAA,QACT;AAAA,QACA,WAAW,wCAAwC;AAAA,QACnD,aAAa,QAAQ,UAAU,gCAAgC;AAAA,QAC/D;AAAA,MAAA;AAAA,MAEF,OAAO,aAAa,OAAO,EAAE,cAAc;AAAA,MAC1C,GAAG,QAAQ;AAAA,MAEZ,UAAA;AAAA,QAAAb,2BAAAA;AAAAA,UAAC;AAAA,UAAA;AAAA,YACC,KAAK,QAAQ;AAAA,YACb,oBAAiB;AAAA,YACjB,WAAWa,MAAAA,GAAG,YAAY,UAAU,CAAC,YAAY,YAAY,QAAQ;AAAA,YACrE,OAAO,WAAW,EAAE,OAAO,UAAU,SAAS,WAAW,UAAU,MAAS;AAAA,YAE5E,UAAA;AAAA,cAAAZ,2BAAAA,IAACa,kBAAAA,wBAAA,EAAuB,OAAO,YAAY,OAAA,CAAgB;AAAA,cAC3Db,2BAAAA;AAAAA,gBAACc,kBAAAA;AAAAA,gBAAA;AAAA,kBACC;AAAA,kBACA;AAAA,kBACA;AAAA,kBACA;AAAA,kBACA;AAAA,kBACA;AAAA,kBACA;AAAA,kBACA,aAAa,MAAM;AAAA,kBAClB,GAAG;AAAA,gBAAA;AAAA,cAAA;AAAA,cAENf,2BAAAA,KAAC,OAAA,EAAI,WAAU,wCACb,UAAA;AAAA,gBAAAC,+BAACe,kBAAAA,0BAAuB,OAAc,QAAgB,OAAc,QAAQ,SAAU,GAAG,eAAe;AAAA,gBACxGf,2BAAAA;AAAAA,kBAACgB,kBAAAA;AAAAA,kBAAA;AAAA,oBACC;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA,aAAa,MAAM;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACrB,EAAA,CACF;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAED,YAAYhB,2BAAAA,IAAC,cAAA,EAAa,SAAkB,YAAY,gBAAgB,OAAA,CAAW;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG1F;;"}
|
|
1
|
+
{"version":3,"file":"GraphDiagram.cjs","sources":["../../src/data/GraphDiagram.tsx"],"sourcesContent":["import { useId, useMemo, type CSSProperties } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { IconButton } from \"../components/IconButton\";\nimport { UiFullscreen, UiHome, UiZoomIn, UiZoomOut } from \"../icons\";\nimport { columnsLayout, type ColumnsLayoutResult } from \"./graph-columns-layout\";\nimport { ringLayout, routeEdges, type GraphLayoutPosition } from \"./graph-layout\";\nimport { GraphDiagramEdgeLabels, GraphDiagramEdgePaths } from \"./GraphDiagramEdges\";\nimport { GraphDiagramGroupBoxes } from \"./GraphDiagramGroups\";\nimport { GraphDiagramNodes } from \"./GraphDiagramNodes\";\nimport { useGroupWindows } from \"./use-group-windows\";\nimport {\n requireEntry,\n type GraphDiagramEdge,\n type GraphDiagramEdgeFocus,\n type GraphDiagramGroup,\n type GraphDiagramLayout,\n type GraphDiagramNode,\n type GraphPlacer,\n} from \"./graph-diagram-model\";\nimport { useEdgeFocus } from \"./use-edge-focus\";\nimport { usePanZoom, type FitOptions, type PanZoom, type WheelMode } from \"./use-pan-zoom\";\n\nexport type {\n GraphDiagramEdge,\n GraphDiagramEdgeFocus,\n GraphDiagramGroup,\n GraphDiagramLayout,\n GraphDiagramNode,\n} from \"./graph-diagram-model\";\n\n// GraphDiagram renders a generic, type-agnostic node/edge diagram. It knows\n// nothing about the domain: a producer maps its data into\n// GraphDiagramNode/GraphDiagramEdge and gets layout, edge routing and\n// selection behaviour.\n//\n// \"ring\" suits a small cycle of resources and processes — relationships with\n// back edges or reciprocal pairs. \"columns\" suits a directed graph with a\n// natural depth (a call graph, a dependency chain): nodes sit in the column of\n// their `level`, grouped and boxed by `group`.\n//\n// The \"columns\" layout, and any zoomable diagram, is drawn at its natural\n// pixel size so node boxes, edges and group boxes share one coordinate system\n// at every container width and zoom level. A non-zoomable \"ring\" keeps scaling\n// its node positions to the container width.\n\nexport interface GraphDiagramProps {\n nodes: GraphDiagramNode[];\n edges: GraphDiagramEdge[];\n /** Defaults to \"ring\". \"columns\" needs a `level` on every node. */\n layout?: GraphDiagramLayout;\n selectedId?: string;\n onNodeSelect?: (id: string) => void;\n /** Called by a node's `+N` control (see `expandCount`); does not select the node. */\n onNodeExpand?: (id: string) => void;\n selectedEdgeId?: string;\n /** Makes edges, and their label pills, selectable. */\n onEdgeSelect?: (id: string) => void;\n /**\n * Whether an edge shows its pill only while in focus, and fades while another is. Defaults to\n * \"auto\": a diagram with more than 32 edges is focused, a smaller one shows everything.\n */\n edgeFocus?: GraphDiagramEdgeFocus;\n /**\n * \"columns\" layout: captions for the group rectangles. A group without an entry shows its id. A group\n * with more than `GROUP_MEMBER_WINDOW` members in a column scrolls them in a window that many rows tall.\n */\n groups?: GraphDiagramGroup[];\n /** \"columns\" layout: the chevron of a group whose `collapsed` is set asks to collapse or expand it. */\n onGroupToggle?: (groupId: string) => void;\n nodeWidth?: number;\n nodeHeight?: number;\n /**\n * \"columns\" layout: the height of a node with `size: \"compact\"`, drawn on one line. A group of only\n * compact nodes stacks them closer and pads them tighter. Defaults to 24.\n */\n compactNodeHeight?: number;\n /** \"columns\" layout: height of a record group's header in px. Defaults to 24. */\n recordHeaderHeight?: number;\n /** \"columns\" layout: horizontal gap between columns in px. Leave room for edge label pills. */\n columnGap?: number;\n /** \"columns\" layout: vertical gap between stacked nodes in px. */\n rowGap?: number;\n /** Ctrl/meta + wheel (or pinch) zooms, dragging the background pans, and zoom controls appear. */\n zoomable?: boolean;\n /** Zoomable: \"zoom\" makes a plain mouse wheel zoom too, for a diagram that fills its pane. Defaults to \"modifier\". */\n wheelZoom?: WheelMode;\n /**\n * Zoomable: the smallest scale the diagram opens at, so its text stays readable. A diagram too\n * large for it opens overflowing, centred on `focusId`, and is panned to reach the rest. `Fit to\n * view` still shows everything, and `Reset view` returns here. Unset, the diagram opens whole.\n */\n fitMinScale?: number;\n /** Zoomable: the node an overflowing diagram opens centred on. Defaults to the diagram's centre. */\n focusId?: string;\n maxHeight?: number | string;\n ariaLabel: string;\n className?: string;\n}\n\nconst DEFAULT_NODE_WIDTH = 168;\nconst DEFAULT_NODE_HEIGHT = 60;\nconst DEFAULT_COMPACT_NODE_HEIGHT = 24;\n\ninterface LayoutOptions {\n layout: GraphDiagramLayout;\n nodeWidth: number;\n nodeHeight: number;\n compactNodeHeight: number;\n records: readonly string[];\n collapsed: readonly string[];\n recordHeaderHeight: number;\n columnGap: number | undefined;\n rowGap: number | undefined;\n}\n\n/**\n * The heights of the nodes not drawn at the node height, in the \"columns\" layout: the compact ones, and\n * the stand-ins of collapsed groups, as tall as the header they are drawn as.\n */\nfunction compactHeights(nodes: GraphDiagramNode[], layout: GraphDiagramLayout, compactNodeHeight: number, standIns: ReadonlySet<string>, headerHeight: number): Record<string, number> {\n if (layout !== \"columns\") return {};\n return Object.fromEntries(nodes.flatMap((node): [string, number][] => {\n if (standIns.has(node.id)) return [[node.id, headerHeight]];\n return node.size === \"compact\" ? [[node.id, compactNodeHeight]] : [];\n }));\n}\n\n/** The one node each collapsed group holds, by group id. */\nfunction collapsedStandIns(nodes: GraphDiagramNode[], groups: GraphDiagramGroup[] | undefined): Map<string, string> {\n const collapsed = new Set(groups?.filter((group) => group.collapsed === true).map((group) => group.id));\n const standIns = new Map<string, string>();\n for (const node of nodes) {\n if (node.group === undefined || !collapsed.has(node.group)) continue;\n const held = standIns.get(node.group);\n if (held !== undefined) throw new Error(`GraphDiagram: collapsed group \"${node.group}\" holds \"${held}\" and \"${node.id}\"; it draws one stand-in`);\n standIns.set(node.group, node.id);\n }\n return standIns;\n}\n\nfunction assertEdgesReferenceKnownNodes(nodes: GraphDiagramNode[], edges: GraphDiagramEdge[]): void {\n const ids = new Set(nodes.map((node) => node.id));\n for (const edge of edges) {\n if (!ids.has(edge.from)) {\n throw new Error(`GraphDiagram: edge \"${edge.id}\" references unknown node \"${edge.from}\"`);\n }\n if (!ids.has(edge.to)) {\n throw new Error(`GraphDiagram: edge \"${edge.id}\" references unknown node \"${edge.to}\"`);\n }\n }\n}\n\nfunction requireLevel(node: GraphDiagramNode): number {\n if (node.level === undefined || !Number.isFinite(node.level)) {\n throw new Error(`GraphDiagram: node \"${node.id}\" needs a finite level for the \"columns\" layout`);\n }\n return node.level;\n}\n\nfunction layoutNodes(\n nodes: GraphDiagramNode[],\n edges: GraphDiagramEdge[],\n { layout, nodeWidth, nodeHeight, compactNodeHeight, records, collapsed, recordHeaderHeight, columnGap, rowGap }: LayoutOptions,\n): ColumnsLayoutResult {\n switch (layout) {\n case \"ring\":\n return { ...ringLayout(nodes, { nodeWidth, nodeHeight }), groups: [] };\n case \"columns\":\n return columnsLayout(\n nodes.map((node) => ({\n id: node.id,\n level: requireLevel(node),\n ...(node.group !== undefined ? { group: node.group } : {}),\n ...(node.size === \"compact\" ? { compact: true } : {}),\n })),\n edges,\n {\n nodeWidth,\n nodeHeight,\n compactNodeHeight,\n records,\n collapsed,\n recordHeaderHeight,\n ...(columnGap !== undefined ? { columnGap } : {}),\n ...(rowGap !== undefined ? { rowGap } : {}),\n },\n );\n default:\n throw new Error(`GraphDiagram: unknown layout \"${layout satisfies never}\"`);\n }\n}\n\nfunction toPercent(value: number, extent: number): string {\n return `${(value / extent) * 100}%`;\n}\n\nfunction stageStyle(size: { width: number; height: number }, natural: boolean, panZoom: PanZoom | undefined): CSSProperties {\n if (!natural) return { aspectRatio: `${size.width} / ${size.height}` };\n if (!panZoom) return size;\n const { x, y, scale } = panZoom.transform;\n return { ...size, transform: `translate(${x}px, ${y}px) scale(${scale})`, transformOrigin: \"0 0\" };\n}\n\n/** `resettable`: the diagram opens at another view than the whole of it, so there is one to return to. */\nfunction ZoomControls({ panZoom, resettable }: { panZoom: PanZoom; resettable: boolean }) {\n return (\n <div className=\"absolute right-2 top-2 flex cursor-default gap-0.5 rounded-md border border-border bg-background p-0.5 shadow-sm\">\n <IconButton icon={UiZoomOut} label=\"Zoom out\" className=\"size-6\" onClick={panZoom.zoomOut} />\n <IconButton icon={UiZoomIn} label=\"Zoom in\" className=\"size-6\" onClick={panZoom.zoomIn} />\n <IconButton icon={UiFullscreen} label=\"Fit to view\" className=\"size-6\" onClick={panZoom.fit} />\n {resettable && <IconButton icon={UiHome} label=\"Reset view\" className=\"size-6\" onClick={panZoom.reset} />}\n </div>\n );\n}\n\n/** How a zoomable diagram opens: no smaller than `fitMinScale`, centred on the `focusId` node. */\nfunction openingFit(\n positions: Record<string, GraphLayoutPosition>,\n fitMinScale: number | undefined,\n focusId: string | undefined,\n): FitOptions {\n return {\n ...(fitMinScale !== undefined ? { minScale: fitMinScale } : {}),\n ...(focusId !== undefined ? { focus: requireEntry(positions, focusId, \"layout position\") } : {}),\n };\n}\n\nexport function GraphDiagram({\n nodes,\n edges,\n layout = \"ring\",\n selectedId,\n onNodeSelect,\n onNodeExpand,\n selectedEdgeId,\n onEdgeSelect,\n edgeFocus = \"auto\",\n groups,\n onGroupToggle,\n nodeWidth = DEFAULT_NODE_WIDTH,\n nodeHeight = DEFAULT_NODE_HEIGHT,\n compactNodeHeight = DEFAULT_COMPACT_NODE_HEIGHT,\n recordHeaderHeight = 24,\n columnGap,\n rowGap,\n zoomable = false,\n wheelZoom = \"modifier\",\n fitMinScale,\n focusId,\n maxHeight,\n ariaLabel,\n className,\n}: GraphDiagramProps) {\n assertEdgesReferenceKnownNodes(nodes, edges);\n const markerPrefix = `graph-diagram-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n const records = useMemo(() => groups?.filter((group) => group.variant === \"record\").map((group) => group.id) ?? [], [groups]);\n const recordNodes = useMemo(() => new Set(layout === \"columns\" ? nodes.filter((node) => node.group !== undefined && records.includes(node.group)).map((node) => node.id) : []), [layout, nodes, records]);\n const standIns = useMemo(() => (layout === \"columns\" ? collapsedStandIns(nodes, groups) : new Map<string, string>()), [layout, nodes, groups]);\n const collapsed = useMemo(() => [...standIns.keys()], [standIns]);\n\n const { width, height, positions: laidOut, groups: groupBoxes } = useMemo(\n () => layoutNodes(nodes, edges, { layout, nodeWidth, nodeHeight, compactNodeHeight, records, collapsed, recordHeaderHeight, columnGap, rowGap }),\n [layout, nodes, edges, nodeWidth, nodeHeight, compactNodeHeight, records, collapsed, recordHeaderHeight, columnGap, rowGap],\n );\n const nodeHeights = useMemo(\n () => compactHeights(nodes, layout, compactNodeHeight, new Set(standIns.values()), recordHeaderHeight),\n [nodes, layout, compactNodeHeight, standIns, recordHeaderHeight],\n );\n\n const panZoom = usePanZoom({\n enabled: zoomable,\n wheel: wheelZoom,\n contentWidth: width,\n contentHeight: height,\n opening: openingFit(laidOut, fitMinScale, focusId),\n });\n const windows = useGroupWindows({ boxes: groupBoxes, positions: laidOut, reveal: selectedId ?? focusId, stageRef: panZoom.contentRef, scale: zoomable ? panZoom.transform.scale : 1 });\n const { positions } = windows;\n const routes = useMemo(\n () =>\n routeEdges(edges, positions, { nodeWidth, nodeHeight, nodeHeights }, { anchor: layout === \"columns\" ? \"side\" : \"center\" }),\n [edges, positions, nodeWidth, nodeHeight, nodeHeights, layout],\n );\n const drawnNodes = useMemo(() => {\n const standing = new Set(standIns.values());\n return nodes.filter((node) => !windows.hidden.has(node.id) && !standing.has(node.id));\n }, [nodes, windows.hidden, standIns]);\n const windowOf = useMemo(\n () => new Map(groupBoxes.flatMap((box) => (box.window ? box.window.members.map((id): [string, string] => [id, box.id]) : []))),\n [groupBoxes],\n );\n const natural = layout === \"columns\" || zoomable;\n const place: GraphPlacer = natural\n ? ({ x, y }) => ({ left: x, top: y })\n : ({ x, y }) => ({ left: toPercent(x, width), top: toPercent(y, height) });\n const focus = useEdgeFocus({ mode: edgeFocus, edges, selectedId, selectedEdgeId });\n const edgeSelection = { selectedEdgeId, onEdgeSelect, focus: focus.state };\n\n return (\n <div\n ref={panZoom.viewportRef}\n className={cn(\n \"w-full\",\n zoomable ? \"relative touch-none overflow-hidden\" : \"overflow-auto\",\n zoomable && (panZoom.panning ? \"cursor-grabbing select-none\" : \"cursor-grab\"),\n className,\n )}\n style={maxHeight != null ? { maxHeight } : undefined}\n {...panZoom.handlers}\n >\n <div\n ref={panZoom.contentRef}\n data-graph-stage=\"\"\n className={cn(\"relative\", natural ? !zoomable && \"mx-auto\" : \"w-full\")}\n style={stageStyle({ width, height }, natural, zoomable ? panZoom : undefined)}\n >\n <GraphDiagramGroupBoxes boxes={groupBoxes} groups={groups} starts={windows.starts} onScroll={windows.scroll} onGroupToggle={onGroupToggle}\n standIns={standIns} selectedId={selectedId} onNodeSelect={onNodeSelect} />\n <GraphDiagramEdgePaths\n edges={edges}\n nodes={nodes}\n routes={routes}\n width={width}\n height={height}\n ariaLabel={ariaLabel}\n markerPrefix={markerPrefix}\n onEdgeHover={focus.onEdgeHover}\n {...edgeSelection}\n />\n <div className=\"pointer-events-none absolute inset-0\">\n <GraphDiagramEdgeLabels edges={edges} routes={routes} place={place} spread={natural} {...edgeSelection} />\n <GraphDiagramNodes\n nodes={drawnNodes}\n positions={positions}\n windowOf={windowOf}\n place={place}\n nodeWidth={nodeWidth}\n nodeHeight={nodeHeight}\n nodeHeights={nodeHeights}\n recordNodes={recordNodes}\n selectedId={selectedId}\n onNodeSelect={onNodeSelect}\n onNodeExpand={onNodeExpand}\n onNodeHover={focus.onNodeHover}\n />\n </div>\n </div>\n {zoomable && <ZoomControls panZoom={panZoom} resettable={fitMinScale !== undefined} />}\n </div>\n );\n}\n"],"names":["ringLayout","columnsLayout","jsxs","jsx","IconButton","UiZoomOut","UiZoomIn","UiFullscreen","UiHome","requireEntry","useId","useMemo","usePanZoom","useGroupWindows","routeEdges","useEdgeFocus","cn","GraphDiagramGroupBoxes","GraphDiagramEdgePaths","GraphDiagramEdgeLabels","GraphDiagramNodes"],"mappings":"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"}
|
|
@@ -18,8 +18,13 @@ export interface GraphDiagramProps {
|
|
|
18
18
|
* "auto": a diagram with more than 32 edges is focused, a smaller one shows everything.
|
|
19
19
|
*/
|
|
20
20
|
edgeFocus?: GraphDiagramEdgeFocus;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* "columns" layout: captions for the group rectangles. A group without an entry shows its id. A group
|
|
23
|
+
* with more than `GROUP_MEMBER_WINDOW` members in a column scrolls them in a window that many rows tall.
|
|
24
|
+
*/
|
|
22
25
|
groups?: GraphDiagramGroup[];
|
|
26
|
+
/** "columns" layout: the chevron of a group whose `collapsed` is set asks to collapse or expand it. */
|
|
27
|
+
onGroupToggle?: (groupId: string) => void;
|
|
23
28
|
nodeWidth?: number;
|
|
24
29
|
nodeHeight?: number;
|
|
25
30
|
/**
|
|
@@ -27,6 +32,8 @@ export interface GraphDiagramProps {
|
|
|
27
32
|
* compact nodes stacks them closer and pads them tighter. Defaults to 24.
|
|
28
33
|
*/
|
|
29
34
|
compactNodeHeight?: number;
|
|
35
|
+
/** "columns" layout: height of a record group's header in px. Defaults to 24. */
|
|
36
|
+
recordHeaderHeight?: number;
|
|
30
37
|
/** "columns" layout: horizontal gap between columns in px. Leave room for edge label pills. */
|
|
31
38
|
columnGap?: number;
|
|
32
39
|
/** "columns" layout: vertical gap between stacked nodes in px. */
|
|
@@ -47,5 +54,5 @@ export interface GraphDiagramProps {
|
|
|
47
54
|
ariaLabel: string;
|
|
48
55
|
className?: string;
|
|
49
56
|
}
|
|
50
|
-
export declare function GraphDiagram({ nodes, edges, layout, selectedId, onNodeSelect, onNodeExpand, selectedEdgeId, onEdgeSelect, edgeFocus, groups, nodeWidth, nodeHeight, compactNodeHeight, columnGap, rowGap, zoomable, wheelZoom, fitMinScale, focusId, maxHeight, ariaLabel, className, }: GraphDiagramProps): import("react").JSX.Element;
|
|
57
|
+
export declare function GraphDiagram({ nodes, edges, layout, selectedId, onNodeSelect, onNodeExpand, selectedEdgeId, onEdgeSelect, edgeFocus, groups, onGroupToggle, nodeWidth, nodeHeight, compactNodeHeight, recordHeaderHeight, columnGap, rowGap, zoomable, wheelZoom, fitMinScale, focusId, maxHeight, ariaLabel, className, }: GraphDiagramProps): import("react").JSX.Element;
|
|
51
58
|
//# sourceMappingURL=GraphDiagram.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GraphDiagram.d.ts","sourceRoot":"","sources":["../../src/data/GraphDiagram.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GraphDiagram.d.ts","sourceRoot":"","sources":["../../src/data/GraphDiagram.tsx"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EAEtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAA6C,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3F,YAAY,EACV,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAiB/B,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,mEAAmE;IACnE,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,qFAAqF;IACrF,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC7B,uGAAuG;IACvG,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sHAAsH;IACtH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oGAAoG;IACpG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkID,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,KAAK,EACL,MAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,SAAkB,EAClB,MAAM,EACN,aAAa,EACb,SAA8B,EAC9B,UAAgC,EAChC,iBAA+C,EAC/C,kBAAuB,EACvB,SAAS,EACT,MAAM,EACN,QAAgB,EAChB,SAAsB,EACtB,WAAW,EACX,OAAO,EACP,SAAS,EACT,SAAS,EACT,SAAS,GACV,EAAE,iBAAiB,+BAkGnB"}
|