next-arch-map 0.1.22 → 0.1.24
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/cli.js +74 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/screenshot.d.ts +22 -0
- package/dist/screenshot.js +120 -0
- package/package.json +9 -1
- package/viewer/src/App.tsx +0 -3
- package/viewer/src/Filters.tsx +0 -1
- package/viewer/src/GraphView.tsx +59 -11
- package/viewer/src/NodeDetails.tsx +25 -1
- package/viewer/src/types.ts +1 -2
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { analyzeProject, diffGraphs } from "./index.js";
|
|
6
|
+
import { captureScreenshots, generateParamsTemplate, } from "./screenshot.js";
|
|
6
7
|
import { serve } from "./serve.js";
|
|
7
8
|
import { readJsonFile, writeJsonFile } from "./utils.js";
|
|
8
9
|
async function main() {
|
|
@@ -17,6 +18,28 @@ async function main() {
|
|
|
17
18
|
await serve(options);
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
21
|
+
if (commandOrArg === "screenshot") {
|
|
22
|
+
const options = parseScreenshotArgs(rest);
|
|
23
|
+
if (options.generateParams) {
|
|
24
|
+
generateParamsTemplate({
|
|
25
|
+
graphPath: path.resolve(options.graphPath),
|
|
26
|
+
outPath: path.resolve(options.paramsPath),
|
|
27
|
+
});
|
|
28
|
+
console.log(`params template written to ${options.paramsPath}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!options.baseUrl) {
|
|
32
|
+
throw new Error("The screenshot command requires --base-url <url>.");
|
|
33
|
+
}
|
|
34
|
+
const result = await captureScreenshots({
|
|
35
|
+
baseUrl: options.baseUrl,
|
|
36
|
+
graphPath: path.resolve(options.graphPath),
|
|
37
|
+
outDir: path.resolve(options.outDir),
|
|
38
|
+
paramsPath: path.resolve(options.paramsPath),
|
|
39
|
+
});
|
|
40
|
+
logScreenshotSummary(result);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
20
43
|
if (commandOrArg === "diff") {
|
|
21
44
|
const options = parseDiffArgs(rest);
|
|
22
45
|
const beforePath = path.resolve(options.beforePath);
|
|
@@ -198,6 +221,46 @@ function parseDevArgs(args) {
|
|
|
198
221
|
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
199
222
|
};
|
|
200
223
|
}
|
|
224
|
+
function parseScreenshotArgs(args) {
|
|
225
|
+
let baseUrl = "";
|
|
226
|
+
let graphPath = "arch/graph.full.json";
|
|
227
|
+
let outDir = "arch/screenshots";
|
|
228
|
+
let paramsPath = "arch/screenshot-params.json";
|
|
229
|
+
let generateParams = false;
|
|
230
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
231
|
+
const argument = args[index];
|
|
232
|
+
if (argument === "--base-url" && args[index + 1]) {
|
|
233
|
+
baseUrl = args[index + 1];
|
|
234
|
+
index += 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (argument === "--graph" && args[index + 1]) {
|
|
238
|
+
graphPath = args[index + 1];
|
|
239
|
+
index += 1;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (argument === "--out-dir" && args[index + 1]) {
|
|
243
|
+
outDir = args[index + 1];
|
|
244
|
+
index += 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (argument === "--params" && args[index + 1]) {
|
|
248
|
+
paramsPath = args[index + 1];
|
|
249
|
+
index += 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (argument === "--generate-params") {
|
|
253
|
+
generateParams = true;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (argument === "--help" || argument === "-h") {
|
|
257
|
+
printHelp();
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
261
|
+
}
|
|
262
|
+
return { baseUrl, graphPath, outDir, paramsPath, generateParams };
|
|
263
|
+
}
|
|
201
264
|
async function runDev(options) {
|
|
202
265
|
const children = [];
|
|
203
266
|
const cleanup = () => {
|
|
@@ -292,9 +355,13 @@ function logDiffSummary(diff, beforePath, afterPath, outputFile) {
|
|
|
292
355
|
`removedEdges=${removedEdges}`,
|
|
293
356
|
].join(" "));
|
|
294
357
|
}
|
|
358
|
+
function logScreenshotSummary(result) {
|
|
359
|
+
console.log([`mode=screenshot`, `captured=${result.captured}`, `skipped=${result.skipped}`].join(" "));
|
|
360
|
+
}
|
|
295
361
|
function printHelp() {
|
|
296
362
|
console.log(`next-arch-map analyze [options]
|
|
297
363
|
next-arch-map diff --before <path> --after <path> [--out <path>]
|
|
364
|
+
next-arch-map screenshot [options]
|
|
298
365
|
next-arch-map serve [options]
|
|
299
366
|
next-arch-map dev [options]
|
|
300
367
|
|
|
@@ -308,6 +375,13 @@ Diff options:
|
|
|
308
375
|
--after <path> Path to the updated graph JSON.
|
|
309
376
|
--out <path> Output diff JSON path. Defaults to arch/graph.diff.json.
|
|
310
377
|
|
|
378
|
+
Screenshot options:
|
|
379
|
+
--base-url <url> Base URL of the running app (e.g. http://localhost:3000).
|
|
380
|
+
--graph <path> Path to graph JSON. Defaults to arch/graph.full.json.
|
|
381
|
+
--out-dir <path> Directory for screenshot PNGs. Defaults to arch/screenshots.
|
|
382
|
+
--params <path> Path to params JSON. Defaults to arch/screenshot-params.json.
|
|
383
|
+
--generate-params Generate a params template for dynamic routes and exit.
|
|
384
|
+
|
|
311
385
|
Serve options:
|
|
312
386
|
--project-root <path> Project root to analyze. Defaults to the current working directory.
|
|
313
387
|
--port <number> Port to listen on. Defaults to 4321.
|
package/dist/index.d.ts
CHANGED
|
@@ -16,3 +16,4 @@ export { analyzePagesToEndpoints } from "./analyzers/pagesToEndpoints.js";
|
|
|
16
16
|
export { analyzeEndpointsToDb } from "./analyzers/endpointsToDb.js";
|
|
17
17
|
export { mergeGraphs, mergePartial } from "./merge.js";
|
|
18
18
|
export { getDbModelsForPage, getEndpointsForPage, getPagesForDbModel } from "./query.js";
|
|
19
|
+
export { captureScreenshots, generateParamsTemplate } from "./screenshot.js";
|
package/dist/index.js
CHANGED
|
@@ -21,3 +21,4 @@ export { analyzePagesToEndpoints } from "./analyzers/pagesToEndpoints.js";
|
|
|
21
21
|
export { analyzeEndpointsToDb } from "./analyzers/endpointsToDb.js";
|
|
22
22
|
export { mergeGraphs, mergePartial } from "./merge.js";
|
|
23
23
|
export { getDbModelsForPage, getEndpointsForPage, getPagesForDbModel } from "./query.js";
|
|
24
|
+
export { captureScreenshots, generateParamsTemplate } from "./screenshot.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Graph } from "./model.js";
|
|
2
|
+
export declare function isDynamicRoute(route: string): boolean;
|
|
3
|
+
export declare function extractRouteParamNames(route: string): string[];
|
|
4
|
+
export declare function resolveRoute(route: string, params: Record<string, string>): string;
|
|
5
|
+
export declare function sanitizeFilename(route: string): string;
|
|
6
|
+
export type GenerateParamsOptions = {
|
|
7
|
+
graphPath: string;
|
|
8
|
+
outPath: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function generateParamsTemplate(options: GenerateParamsOptions): void;
|
|
11
|
+
export type CaptureScreenshotsOptions = {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
graphPath: string;
|
|
14
|
+
outDir: string;
|
|
15
|
+
paramsPath?: string;
|
|
16
|
+
};
|
|
17
|
+
export type CaptureResult = {
|
|
18
|
+
graph: Graph;
|
|
19
|
+
captured: number;
|
|
20
|
+
skipped: number;
|
|
21
|
+
};
|
|
22
|
+
export declare function captureScreenshots(options: CaptureScreenshotsOptions): Promise<CaptureResult>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readJsonFile, writeJsonFile } from "./utils.js";
|
|
4
|
+
const DYNAMIC_SEGMENT_PATTERN = /\[[\w.]+\]/;
|
|
5
|
+
const PARAM_SEGMENT_PATTERN = /\[{1,2}(?:\.{3})?(\w+)\]{1,2}/g;
|
|
6
|
+
export function isDynamicRoute(route) {
|
|
7
|
+
return DYNAMIC_SEGMENT_PATTERN.test(route);
|
|
8
|
+
}
|
|
9
|
+
export function extractRouteParamNames(route) {
|
|
10
|
+
const names = [];
|
|
11
|
+
for (const match of route.matchAll(PARAM_SEGMENT_PATTERN)) {
|
|
12
|
+
names.push(match[1]);
|
|
13
|
+
}
|
|
14
|
+
return names;
|
|
15
|
+
}
|
|
16
|
+
export function resolveRoute(route, params) {
|
|
17
|
+
return route.replace(PARAM_SEGMENT_PATTERN, (_match, name) => {
|
|
18
|
+
return params[name] ?? _match;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function sanitizeFilename(route) {
|
|
22
|
+
if (route === "/")
|
|
23
|
+
return "index.png";
|
|
24
|
+
const name = route
|
|
25
|
+
.replace(/^\//, "")
|
|
26
|
+
.replace(/\//g, "-");
|
|
27
|
+
return `${name}.png`;
|
|
28
|
+
}
|
|
29
|
+
export function generateParamsTemplate(options) {
|
|
30
|
+
const graph = readJsonFile(options.graphPath);
|
|
31
|
+
const template = {};
|
|
32
|
+
for (const node of graph.nodes) {
|
|
33
|
+
if (node.type !== "page")
|
|
34
|
+
continue;
|
|
35
|
+
const route = node.meta?.route ?? node.label;
|
|
36
|
+
if (!isDynamicRoute(String(route)))
|
|
37
|
+
continue;
|
|
38
|
+
const paramNames = extractRouteParamNames(String(route));
|
|
39
|
+
const params = {};
|
|
40
|
+
for (const name of paramNames) {
|
|
41
|
+
params[name] = "";
|
|
42
|
+
}
|
|
43
|
+
template[String(route)] = params;
|
|
44
|
+
}
|
|
45
|
+
writeJsonFile(options.outPath, template);
|
|
46
|
+
}
|
|
47
|
+
export async function captureScreenshots(options) {
|
|
48
|
+
let playwright;
|
|
49
|
+
try {
|
|
50
|
+
const moduleName = "playwright";
|
|
51
|
+
playwright = await import(/* webpackIgnore: true */ moduleName);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error("Playwright is required for screenshots. Install it with: npm install playwright");
|
|
55
|
+
}
|
|
56
|
+
const graph = readJsonFile(options.graphPath);
|
|
57
|
+
const params = options.paramsPath &&
|
|
58
|
+
fs.existsSync(options.paramsPath)
|
|
59
|
+
? readJsonFile(options.paramsPath)
|
|
60
|
+
: {};
|
|
61
|
+
const pageNodes = graph.nodes
|
|
62
|
+
.filter((node) => node.type === "page")
|
|
63
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
64
|
+
let captured = 0;
|
|
65
|
+
let skipped = 0;
|
|
66
|
+
const browser = await playwright.chromium.launch();
|
|
67
|
+
const context = await browser.newContext();
|
|
68
|
+
const page = await context.newPage();
|
|
69
|
+
try {
|
|
70
|
+
for (const node of pageNodes) {
|
|
71
|
+
const route = String(node.meta?.route ?? node.label);
|
|
72
|
+
if (isDynamicRoute(route)) {
|
|
73
|
+
const routeParams = params[route];
|
|
74
|
+
if (!routeParams || Object.values(routeParams).some((v) => !v)) {
|
|
75
|
+
console.log(`skipped ${route} (missing params)`);
|
|
76
|
+
skipped += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const resolved = resolveRoute(route, routeParams);
|
|
80
|
+
try {
|
|
81
|
+
await capturePageScreenshot(page, node, resolved, options);
|
|
82
|
+
captured += 1;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
console.log(`failed ${route}: ${message}`);
|
|
87
|
+
skipped += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
try {
|
|
92
|
+
await capturePageScreenshot(page, node, route, options);
|
|
93
|
+
captured += 1;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
console.log(`failed ${route}: ${message}`);
|
|
98
|
+
skipped += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await browser.close();
|
|
105
|
+
}
|
|
106
|
+
writeJsonFile(options.graphPath, graph);
|
|
107
|
+
return { graph, captured, skipped };
|
|
108
|
+
}
|
|
109
|
+
async function capturePageScreenshot(page, node, resolvedRoute, options) {
|
|
110
|
+
const url = options.baseUrl.replace(/\/$/, "") + resolvedRoute;
|
|
111
|
+
await page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
|
|
112
|
+
const buffer = await page.screenshot({ fullPage: true });
|
|
113
|
+
fs.mkdirSync(options.outDir, { recursive: true });
|
|
114
|
+
const filename = sanitizeFilename(resolvedRoute);
|
|
115
|
+
fs.writeFileSync(path.join(options.outDir, filename), buffer);
|
|
116
|
+
if (!node.meta)
|
|
117
|
+
node.meta = {};
|
|
118
|
+
node.meta.screenshot = `data:image/png;base64,${buffer.toString("base64")}`;
|
|
119
|
+
console.log(`captured ${resolvedRoute} → ${filename}`);
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-arch-map",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "Static analyzer that builds a multi-layer architecture graph for Next.js-style apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,14 @@
|
|
|
47
47
|
"chokidar": "^3.6.0",
|
|
48
48
|
"typescript": "^5.8.2"
|
|
49
49
|
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"playwright": ">=1.40.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"playwright": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
50
58
|
"devDependencies": {
|
|
51
59
|
"@eslint/js": "^10.0.1",
|
|
52
60
|
"@types/node": "^20.19.0",
|
package/viewer/src/App.tsx
CHANGED
|
@@ -9,15 +9,12 @@ const ALL_NODE_TYPES: NodeType[] = [
|
|
|
9
9
|
"page",
|
|
10
10
|
"endpoint",
|
|
11
11
|
"handler",
|
|
12
|
-
"action",
|
|
13
12
|
"db",
|
|
14
13
|
];
|
|
15
14
|
const ALL_EDGE_KINDS: EdgeKind[] = [
|
|
16
15
|
"page-endpoint",
|
|
17
16
|
"endpoint-db",
|
|
18
17
|
"endpoint-handler",
|
|
19
|
-
"page-action",
|
|
20
|
-
"action-endpoint",
|
|
21
18
|
];
|
|
22
19
|
|
|
23
20
|
|
package/viewer/src/Filters.tsx
CHANGED
package/viewer/src/GraphView.tsx
CHANGED
|
@@ -31,7 +31,6 @@ const NODE_COLOR: Record<NodeType, string> = {
|
|
|
31
31
|
endpoint: "#059669",
|
|
32
32
|
db: "#dc2626",
|
|
33
33
|
handler: "#14b8a6",
|
|
34
|
-
action: "#fbbf24",
|
|
35
34
|
};
|
|
36
35
|
|
|
37
36
|
const NODE_BORDER: Record<NodeType, string> = {
|
|
@@ -39,15 +38,12 @@ const NODE_BORDER: Record<NodeType, string> = {
|
|
|
39
38
|
endpoint: "#047857",
|
|
40
39
|
db: "#b91c1c",
|
|
41
40
|
handler: "#0d9488",
|
|
42
|
-
action: "#f59e0b",
|
|
43
41
|
};
|
|
44
42
|
|
|
45
43
|
const EDGE_COLOR: Record<EdgeKind, string> = {
|
|
46
44
|
"page-endpoint": "#06b6d4",
|
|
47
45
|
"endpoint-db": "#f97316",
|
|
48
46
|
"endpoint-handler": "#22c55e",
|
|
49
|
-
"page-action": "#eab308",
|
|
50
|
-
"action-endpoint": "#a855f7",
|
|
51
47
|
};
|
|
52
48
|
|
|
53
49
|
const DIFF_BORDER_COLOR: Record<DiffStatus, string> = {
|
|
@@ -141,14 +137,34 @@ function reorderColumn(
|
|
|
141
137
|
nodes.forEach((node, i) => nodeRowIndex.set(node.id, i));
|
|
142
138
|
}
|
|
143
139
|
|
|
140
|
+
function DescriptionLine({ text, dark }: { text: string; dark?: boolean }) {
|
|
141
|
+
return (
|
|
142
|
+
<div
|
|
143
|
+
style={{
|
|
144
|
+
marginTop: 3,
|
|
145
|
+
fontSize: 10,
|
|
146
|
+
fontWeight: 400,
|
|
147
|
+
opacity: 0.85,
|
|
148
|
+
lineHeight: 1.3,
|
|
149
|
+
color: dark ? "#1e293b" : "#ffffff",
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{text}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
144
157
|
function PageNode({ data }: NodeProps) {
|
|
145
|
-
const
|
|
158
|
+
const d = data as Record<string, unknown>;
|
|
159
|
+
const screenshot = d.screenshot as string | undefined;
|
|
160
|
+
const description = d.description as string | undefined;
|
|
146
161
|
return (
|
|
147
162
|
<div>
|
|
148
163
|
<Handle type="target" position={Position.Left} style={{ visibility: "hidden" }} />
|
|
149
164
|
<div style={{ fontSize: 12, fontWeight: 600 }}>
|
|
150
|
-
{String(
|
|
165
|
+
{String(d.label ?? "")}
|
|
151
166
|
</div>
|
|
167
|
+
{description && <DescriptionLine text={description} />}
|
|
152
168
|
{screenshot && (
|
|
153
169
|
<img
|
|
154
170
|
src={screenshot}
|
|
@@ -166,7 +182,26 @@ function PageNode({ data }: NodeProps) {
|
|
|
166
182
|
);
|
|
167
183
|
}
|
|
168
184
|
|
|
169
|
-
|
|
185
|
+
function DescribedNode({ data }: NodeProps) {
|
|
186
|
+
const d = data as Record<string, unknown>;
|
|
187
|
+
const description = d.description as string | undefined;
|
|
188
|
+
const dark = d.dark as boolean | undefined;
|
|
189
|
+
return (
|
|
190
|
+
<div>
|
|
191
|
+
<Handle type="target" position={Position.Left} style={{ visibility: "hidden" }} />
|
|
192
|
+
<div style={{ fontSize: 12, fontWeight: 600 }}>
|
|
193
|
+
{String(d.label ?? "")}
|
|
194
|
+
</div>
|
|
195
|
+
{description && <DescriptionLine text={description} dark={dark} />}
|
|
196
|
+
<Handle type="source" position={Position.Right} style={{ visibility: "hidden" }} />
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const nodeTypes = {
|
|
202
|
+
pageNode: PageNode,
|
|
203
|
+
describedNode: DescribedNode,
|
|
204
|
+
};
|
|
170
205
|
|
|
171
206
|
export function GraphView(props: GraphViewProps) {
|
|
172
207
|
const {
|
|
@@ -202,7 +237,7 @@ export function GraphView(props: GraphViewProps) {
|
|
|
202
237
|
|
|
203
238
|
// Compute layout without hover state — this is the expensive part
|
|
204
239
|
const { flowNodes: baseFlowNodes, flowEdges: baseFlowEdges } = useMemo(() => {
|
|
205
|
-
const typeOrder: NodeType[] = ["page", "
|
|
240
|
+
const typeOrder: NodeType[] = ["page", "endpoint", "handler", "db"];
|
|
206
241
|
const visibleNodes = graph.nodes.filter((node) => visibleNodeTypes.has(node.type));
|
|
207
242
|
const visibleNodeIds = new Set(visibleNodes.map((node) => node.id));
|
|
208
243
|
const nodesByType = new Map<NodeType, typeof visibleNodes>(
|
|
@@ -238,11 +273,24 @@ export function GraphView(props: GraphViewProps) {
|
|
|
238
273
|
const borderStyle = status === "removed" ? "dashed" : "solid";
|
|
239
274
|
const isPage = node.type === "page";
|
|
240
275
|
const screenshot = isPage ? (node.meta?.screenshot as string | undefined) : undefined;
|
|
276
|
+
const description = node.meta?.description as string | undefined;
|
|
277
|
+
const isDarkText = false;
|
|
278
|
+
|
|
279
|
+
const nodeType = isPage
|
|
280
|
+
? "pageNode"
|
|
281
|
+
: description
|
|
282
|
+
? "describedNode"
|
|
283
|
+
: undefined;
|
|
241
284
|
|
|
242
285
|
flowNodes.push({
|
|
243
286
|
id: node.id,
|
|
244
|
-
...(
|
|
245
|
-
data: {
|
|
287
|
+
...(nodeType ? { type: nodeType } : {}),
|
|
288
|
+
data: {
|
|
289
|
+
label: node.label,
|
|
290
|
+
...(screenshot ? { screenshot } : {}),
|
|
291
|
+
...(description ? { description } : {}),
|
|
292
|
+
...(isDarkText ? { dark: true } : {}),
|
|
293
|
+
},
|
|
246
294
|
position: {
|
|
247
295
|
x: 80 + columnIndex * columnWidth,
|
|
248
296
|
y: 80 + rowIndex * rowHeight,
|
|
@@ -256,7 +304,7 @@ export function GraphView(props: GraphViewProps) {
|
|
|
256
304
|
border: `2px ${borderStyle} ${borderColor}`,
|
|
257
305
|
padding: "10px 14px",
|
|
258
306
|
background: NODE_COLOR[node.type],
|
|
259
|
-
color:
|
|
307
|
+
color: "#ffffff",
|
|
260
308
|
fontSize: 12,
|
|
261
309
|
fontWeight: 600,
|
|
262
310
|
fontFamily: "'Inter', -apple-system, sans-serif",
|
|
@@ -46,6 +46,12 @@ export function NodeDetails({ node }: NodeDetailsProps) {
|
|
|
46
46
|
{node.id}
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
|
+
{(node.meta?.descriptionLong || node.meta?.description) && (
|
|
50
|
+
<p className="text-xs text-slate-600 leading-relaxed">
|
|
51
|
+
{String(node.meta.descriptionLong ?? node.meta.description)}
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
54
|
+
|
|
49
55
|
{filePath !== undefined && filePath !== null && (
|
|
50
56
|
<div className="text-[11px] text-slate-500">
|
|
51
57
|
<span className="text-slate-400">file: </span>
|
|
@@ -53,9 +59,27 @@ export function NodeDetails({ node }: NodeDetailsProps) {
|
|
|
53
59
|
</div>
|
|
54
60
|
)}
|
|
55
61
|
|
|
62
|
+
{node.meta?.screenshot && (
|
|
63
|
+
<div className="mt-2">
|
|
64
|
+
<img
|
|
65
|
+
src={String(node.meta.screenshot)}
|
|
66
|
+
alt={`Screenshot of ${node.label}`}
|
|
67
|
+
className="w-full rounded-md border border-slate-200"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
|
|
56
72
|
{node.meta && (
|
|
57
73
|
<pre className="mt-2 p-2.5 rounded-md bg-slate-50 border border-slate-100 text-[11px] font-mono text-slate-600 max-h-40 overflow-auto whitespace-pre-wrap break-all">
|
|
58
|
-
{JSON.stringify(
|
|
74
|
+
{JSON.stringify(
|
|
75
|
+
Object.fromEntries(
|
|
76
|
+
Object.entries(node.meta).filter(
|
|
77
|
+
([key]) => key !== "screenshot" && key !== "description" && key !== "descriptionLong",
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
null,
|
|
81
|
+
2,
|
|
82
|
+
)}
|
|
59
83
|
</pre>
|
|
60
84
|
)}
|
|
61
85
|
</div>
|