next-arch-map 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ import type { EdgeKind, NodeType } from "./types";
2
+
3
+ type FiltersProps = {
4
+ allNodeTypes: NodeType[];
5
+ allEdgeKinds: EdgeKind[];
6
+ visibleNodeTypes: Set<NodeType>;
7
+ visibleEdgeKinds: Set<EdgeKind>;
8
+ onToggleNodeType: (type: NodeType) => void;
9
+ onToggleEdgeKind: (kind: EdgeKind) => void;
10
+ };
11
+
12
+ export function Filters(props: FiltersProps) {
13
+ const {
14
+ allNodeTypes,
15
+ allEdgeKinds,
16
+ visibleNodeTypes,
17
+ visibleEdgeKinds,
18
+ onToggleNodeType,
19
+ onToggleEdgeKind,
20
+ } = props;
21
+
22
+ return (
23
+ <div>
24
+ <h2 style={{ fontSize: 14, marginBottom: 8 }}>Node types</h2>
25
+ {allNodeTypes.map((type) => (
26
+ <label key={type} style={{ display: "block", fontSize: 13, marginBottom: 4 }}>
27
+ <input
28
+ type="checkbox"
29
+ checked={visibleNodeTypes.has(type)}
30
+ onChange={() => onToggleNodeType(type)}
31
+ />{" "}
32
+ {type}
33
+ </label>
34
+ ))}
35
+
36
+ <h2 style={{ fontSize: 14, marginTop: 16, marginBottom: 8 }}>Edge kinds</h2>
37
+ {allEdgeKinds.map((kind) => (
38
+ <label key={kind} style={{ display: "block", fontSize: 13, marginBottom: 4 }}>
39
+ <input
40
+ type="checkbox"
41
+ checked={visibleEdgeKinds.has(kind)}
42
+ onChange={() => onToggleEdgeKind(kind)}
43
+ />{" "}
44
+ {kind}
45
+ </label>
46
+ ))}
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,200 @@
1
+ import { useMemo } from "react";
2
+ import {
3
+ Background,
4
+ Controls,
5
+ MarkerType,
6
+ MiniMap,
7
+ Position,
8
+ ReactFlow,
9
+ type Edge as FlowEdge,
10
+ type Node as FlowNode,
11
+ type NodeMouseHandler,
12
+ } from "@xyflow/react";
13
+ import "@xyflow/react/dist/style.css";
14
+ import type { DiffStatus, EdgeKind, Graph, NodeType } from "./types";
15
+
16
+ type GraphViewProps = {
17
+ graph: Graph;
18
+ visibleNodeTypes: Set<NodeType>;
19
+ visibleEdgeKinds: Set<EdgeKind>;
20
+ selectedNodeId: string | null;
21
+ onSelectNode: (nodeId: string | null) => void;
22
+ nodeStatusById?: Map<string, DiffStatus>;
23
+ edgeStatusByKey?: Map<string, DiffStatus>;
24
+ };
25
+
26
+ const NODE_COLOR: Record<NodeType, string> = {
27
+ page: "#1d4ed8",
28
+ endpoint: "#047857",
29
+ db: "#b91c1c",
30
+ ui: "#b45309",
31
+ handler: "#0d9488",
32
+ action: "#facc15",
33
+ };
34
+
35
+ const EDGE_COLOR: Record<EdgeKind, string> = {
36
+ "page-endpoint": "#0891b2",
37
+ "endpoint-db": "#ea580c",
38
+ "page-ui": "#7c3aed",
39
+ "endpoint-handler": "#22c55e",
40
+ "page-action": "#facc15",
41
+ "action-endpoint": "#a855f7",
42
+ };
43
+
44
+ const DIFF_BORDER_COLOR: Record<DiffStatus, string> = {
45
+ added: "#22c55e",
46
+ removed: "#ef4444",
47
+ unchanged: "rgba(15, 23, 42, 0.18)",
48
+ };
49
+
50
+ const DIFF_EDGE_COLOR: Record<DiffStatus, string> = {
51
+ added: "#22c55e",
52
+ removed: "#ef4444",
53
+ unchanged: "#000000",
54
+ };
55
+
56
+ function buildEdgeKey(from: string, to: string, kind: EdgeKind): string {
57
+ return `${from}::${to}::${kind}`;
58
+ }
59
+
60
+ export function GraphView(props: GraphViewProps) {
61
+ const {
62
+ graph,
63
+ visibleNodeTypes,
64
+ visibleEdgeKinds,
65
+ selectedNodeId,
66
+ onSelectNode,
67
+ nodeStatusById,
68
+ edgeStatusByKey,
69
+ } = props;
70
+ const handleNodeClick: NodeMouseHandler = (_event, node) => onSelectNode(node.id);
71
+
72
+ const { flowNodes, flowEdges } = useMemo(() => {
73
+ const typeOrder: NodeType[] = ["page", "action", "endpoint", "handler", "db", "ui"];
74
+ const visibleNodes = graph.nodes.filter((node) => visibleNodeTypes.has(node.type));
75
+ const visibleNodeIds = new Set(visibleNodes.map((node) => node.id));
76
+ const nodesByType = new Map<NodeType, typeof visibleNodes>(
77
+ typeOrder.map((type) => [type, []]),
78
+ );
79
+
80
+ for (const node of visibleNodes) {
81
+ nodesByType.get(node.type)?.push(node);
82
+ }
83
+
84
+ for (const nodes of nodesByType.values()) {
85
+ nodes.sort((left, right) => left.label.localeCompare(right.label));
86
+ }
87
+
88
+ const activeTypeOrder = typeOrder.filter((type) => (nodesByType.get(type)?.length ?? 0) > 0);
89
+
90
+ const flowNodes: FlowNode[] = [];
91
+ const columnWidth = 280;
92
+ const rowHeight = 94;
93
+
94
+ activeTypeOrder.forEach((type, columnIndex) => {
95
+ const nodes = nodesByType.get(type) ?? [];
96
+ nodes.forEach((node, rowIndex) => {
97
+ const isSelected = node.id === selectedNodeId;
98
+ const status = nodeStatusById?.get(node.id) ?? "unchanged";
99
+ const borderColor = DIFF_BORDER_COLOR[status];
100
+ const borderStyle = status === "removed" ? "dashed" : "solid";
101
+
102
+ flowNodes.push({
103
+ id: node.id,
104
+ data: { label: node.label },
105
+ position: {
106
+ x: 80 + columnIndex * columnWidth,
107
+ y: 80 + rowIndex * rowHeight,
108
+ },
109
+ sourcePosition: Position.Right,
110
+ targetPosition: Position.Left,
111
+ selectable: true,
112
+ style: {
113
+ width: 190,
114
+ borderRadius: 10,
115
+ border: `${isSelected ? 3 : 2}px ${borderStyle} ${isSelected ? "#111827" : borderColor}`,
116
+ padding: 10,
117
+ background: NODE_COLOR[node.type],
118
+ color: node.type === "action" ? "#111827" : "#fff",
119
+ fontSize: 12,
120
+ fontWeight: 600,
121
+ opacity: status === "removed" ? 0.78 : 1,
122
+ boxShadow: isSelected
123
+ ? "0 0 0 4px rgba(15, 23, 42, 0.12)"
124
+ : status === "added"
125
+ ? "0 0 0 3px rgba(34, 197, 94, 0.16)"
126
+ : status === "removed"
127
+ ? "0 0 0 3px rgba(239, 68, 68, 0.12)"
128
+ : "0 10px 30px rgba(15, 23, 42, 0.08)",
129
+ },
130
+ });
131
+ });
132
+ });
133
+
134
+ const flowEdges: FlowEdge[] = graph.edges
135
+ .filter(
136
+ (edge) =>
137
+ visibleEdgeKinds.has(edge.kind) &&
138
+ visibleNodeIds.has(edge.from) &&
139
+ visibleNodeIds.has(edge.to),
140
+ )
141
+ .map((edge, index) => {
142
+ const status = edgeStatusByKey?.get(buildEdgeKey(edge.from, edge.to, edge.kind)) ?? "unchanged";
143
+ const strokeColor = status === "unchanged" ? EDGE_COLOR[edge.kind] : DIFF_EDGE_COLOR[status];
144
+
145
+ return {
146
+ id: `${edge.from}=>${edge.to}::${edge.kind}::${index}`,
147
+ source: edge.from,
148
+ target: edge.to,
149
+ animated: false,
150
+ style: {
151
+ stroke: strokeColor,
152
+ strokeWidth: 1.75,
153
+ strokeDasharray: status === "removed" ? "6 4" : undefined,
154
+ opacity: status === "removed" ? 0.76 : 1,
155
+ },
156
+ markerEnd: {
157
+ type: MarkerType.ArrowClosed,
158
+ color: strokeColor,
159
+ },
160
+ };
161
+ });
162
+
163
+ return { flowNodes, flowEdges };
164
+ }, [
165
+ edgeStatusByKey,
166
+ graph,
167
+ nodeStatusById,
168
+ selectedNodeId,
169
+ visibleEdgeKinds,
170
+ visibleNodeTypes,
171
+ ]);
172
+
173
+ return (
174
+ <div style={{ width: "100%", height: "100%" }}>
175
+ <ReactFlow
176
+ nodes={flowNodes}
177
+ edges={flowEdges}
178
+ fitView
179
+ fitViewOptions={{ padding: 0.18 }}
180
+ onNodeClick={handleNodeClick}
181
+ onPaneClick={() => onSelectNode(null)}
182
+ nodesDraggable={false}
183
+ nodesConnectable={false}
184
+ elementsSelectable
185
+ proOptions={{ hideAttribution: true }}
186
+ >
187
+ <Background gap={18} size={1} color="#e5e7eb" />
188
+ <MiniMap
189
+ pannable
190
+ zoomable
191
+ nodeColor={(node) => {
192
+ const type = graph.nodes.find((graphNode) => graphNode.id === node.id)?.type;
193
+ return type ? NODE_COLOR[type] : "#94a3b8";
194
+ }}
195
+ />
196
+ <Controls />
197
+ </ReactFlow>
198
+ </div>
199
+ );
200
+ }
@@ -0,0 +1,55 @@
1
+ import type { Node } from "./types";
2
+
3
+ type NodeDetailsProps = {
4
+ node: Node | null;
5
+ };
6
+
7
+ export function NodeDetails({ node }: NodeDetailsProps) {
8
+ if (!node) {
9
+ return (
10
+ <div style={{ marginTop: 16, fontSize: 12, color: "#6b7280" }}>
11
+ No node selected.
12
+ </div>
13
+ );
14
+ }
15
+
16
+ const filePath = node.meta?.filePath;
17
+
18
+ return (
19
+ <div style={{ marginTop: 16 }}>
20
+ <h2 style={{ fontSize: 14, marginBottom: 8 }}>Selected node</h2>
21
+ <div style={{ fontSize: 12, lineHeight: 1.5 }}>
22
+ <div>
23
+ <strong>id:</strong> {node.id}
24
+ </div>
25
+ <div>
26
+ <strong>type:</strong> {node.type}
27
+ </div>
28
+ <div>
29
+ <strong>label:</strong> {node.label}
30
+ </div>
31
+ {filePath !== undefined && filePath !== null && (
32
+ <div>
33
+ <strong>file:</strong> {String(filePath)}
34
+ </div>
35
+ )}
36
+ {node.meta && (
37
+ <pre
38
+ style={{
39
+ marginTop: 8,
40
+ padding: 8,
41
+ background: "#f7f7f7",
42
+ borderRadius: 4,
43
+ maxHeight: 160,
44
+ overflow: "auto",
45
+ whiteSpace: "pre-wrap",
46
+ wordBreak: "break-word",
47
+ }}
48
+ >
49
+ {JSON.stringify(node.meta, null, 2)}
50
+ </pre>
51
+ )}
52
+ </div>
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,32 @@
1
+ :root {
2
+ font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
3
+ color: #0f172a;
4
+ background: #f8fafc;
5
+ }
6
+
7
+ * {
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ html,
12
+ body,
13
+ #root {
14
+ margin: 0;
15
+ width: 100%;
16
+ height: 100%;
17
+ }
18
+
19
+ body {
20
+ min-width: 320px;
21
+ }
22
+
23
+ button,
24
+ input,
25
+ textarea,
26
+ select {
27
+ font: inherit;
28
+ }
29
+
30
+ pre {
31
+ margin: 0;
32
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./index.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1,51 @@
1
+ export type NodeType =
2
+ | "page"
3
+ | "endpoint"
4
+ | "db"
5
+ | "ui"
6
+ | "handler"
7
+ | "action";
8
+
9
+ export type Node = {
10
+ id: string;
11
+ type: NodeType;
12
+ label: string;
13
+ meta?: Record<string, unknown>;
14
+ };
15
+
16
+ export type EdgeKind =
17
+ | "page-endpoint"
18
+ | "endpoint-db"
19
+ | "page-ui"
20
+ | "endpoint-handler"
21
+ | "page-action"
22
+ | "action-endpoint";
23
+
24
+ export type Edge = {
25
+ from: string;
26
+ to: string;
27
+ kind: EdgeKind;
28
+ meta?: Record<string, unknown>;
29
+ };
30
+
31
+ export type Graph = {
32
+ nodes: Node[];
33
+ edges: Edge[];
34
+ };
35
+
36
+ export type DiffStatus = "added" | "removed" | "unchanged";
37
+
38
+ export type NodeDiff = {
39
+ node: Node;
40
+ status: DiffStatus;
41
+ };
42
+
43
+ export type EdgeDiff = {
44
+ edge: Edge;
45
+ status: DiffStatus;
46
+ };
47
+
48
+ export type GraphDiff = {
49
+ nodes: NodeDiff[];
50
+ edges: EdgeDiff[];
51
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "Bundler",
9
+ "allowImportingTsExtensions": false,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true
18
+ },
19
+ "include": ["src"]
20
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler",
8
+ "allowSyntheticDefaultImports": true
9
+ },
10
+ "include": ["vite.config.ts"]
11
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ });