tablegraph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # TableGraph
2
+
3
+ Visualize your database schema as an interactive graph directly in your browser.
4
+
5
+ <p>
6
+ <img src="./assets/logo.png" alt="TableGraph logo" width="250" />
7
+ </p>
8
+
9
+ ## What it does
10
+
11
+ TableGraph connects to your database, reads schema metadata, and opens a local web interface where you can inspect tables, columns, and relationships visually.
12
+
13
+ ## Install
14
+
15
+ ### Global install
16
+
17
+ ```bash
18
+ npm install -g tablegraph
19
+ ```
20
+
21
+ ### Or run with npx
22
+
23
+ ```bash
24
+ npx tablegraph --driver postgres --url <connection-url>
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ tablegraph --driver postgres --url postgresql://username:password@localhost:5432/db_name
31
+ ```
32
+
33
+ Then open:
34
+
35
+ ```txt
36
+ http://localhost:3001
37
+ ```
38
+
39
+ ## Example
40
+
41
+ ```bash
42
+ tablegraph --driver postgres --url postgresql://postgres:postgres@localhost:5434/postgres
43
+ ```
44
+
45
+ ## Visualization
46
+
47
+ <p align="center">
48
+ <img src="./assets/example.png" alt="TableGraph web visualization" />
49
+ </p>
50
+
51
+ ## Supported drivers
52
+
53
+ - postgres
54
+
55
+ ## Notes
56
+
57
+ - Make sure your database is running and accessible
58
+ - Ensure your schema is initialized and contains tables
59
+ - TableGraph runs locally and serves the visualization in your browser
Binary file
Binary file
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import { getDatabaseInfo, supportedDriversList, } from "../core/index.js";
3
+ import { startServer } from "../server/index.js";
4
+ const cliFlags = ["--driver", "--url", "--help"];
5
+ const cliFlagsSet = new Set(cliFlags);
6
+ const supportedDriversSet = new Set(supportedDriversList);
7
+ function invalidUrl(value) {
8
+ try {
9
+ new URL(value);
10
+ return false;
11
+ }
12
+ catch {
13
+ return true;
14
+ }
15
+ }
16
+ function printHelp() {
17
+ console.log(`
18
+ Usage:
19
+ tablegraph [options]
20
+
21
+ Options:
22
+ --driver <postgres>
23
+ --url <connection-url>
24
+ --help
25
+
26
+ Examples:
27
+ tablegraph --driver postgres --url postgresql://username:password@localhost:5432/db_name
28
+ `.trim());
29
+ }
30
+ function parseArgs(args) {
31
+ const options = {};
32
+ if (args.includes("--help")) {
33
+ printHelp();
34
+ process.exit(0);
35
+ }
36
+ if (args.length === 0) {
37
+ printHelp();
38
+ process.exit(1);
39
+ }
40
+ if (args.length % 2 !== 0) {
41
+ throw new Error("Flags must be provided as pairs: --flag value.");
42
+ }
43
+ for (let i = 0; i < args.length; i += 2) {
44
+ const flag = args[i];
45
+ const value = args[i + 1];
46
+ if (!cliFlagsSet.has(flag)) {
47
+ throw new Error(`Unsupported flag "${flag}". Valid flags: ${cliFlags.join(", ")}.`);
48
+ }
49
+ if (flag === "--help") {
50
+ continue;
51
+ }
52
+ if (!value || value.startsWith("--")) {
53
+ throw new Error(`Missing value for flag "${flag}".`);
54
+ }
55
+ if (flag === "--driver") {
56
+ if (!supportedDriversSet.has(value)) {
57
+ throw new Error(`Unsupported driver "${value}". Valid drivers: ${supportedDriversList.join(", ")}.`);
58
+ }
59
+ options.driver = value;
60
+ continue;
61
+ }
62
+ if (flag === "--url") {
63
+ if (invalidUrl(value)) {
64
+ throw new Error(`Invalid URL "${value}".`);
65
+ }
66
+ options.url = value;
67
+ }
68
+ }
69
+ if (!options.driver) {
70
+ throw new Error('Missing required flag "--driver".');
71
+ }
72
+ if (!options.url) {
73
+ throw new Error('Missing required flag "--url".');
74
+ }
75
+ return {
76
+ driver: options.driver,
77
+ url: options.url,
78
+ };
79
+ }
80
+ async function main() {
81
+ const argList = process.argv.slice(2);
82
+ const options = parseArgs(argList);
83
+ const dbInfo = await getDatabaseInfo(options.driver, options.url);
84
+ if (dbInfo.tables.length === 0) {
85
+ console.warn([
86
+ "Warning: no tables were found in the database.",
87
+ "Make sure you are connecting to the correct database and that your schema has already been initialized.",
88
+ ].join("\n"));
89
+ }
90
+ await startServer(dbInfo);
91
+ }
92
+ main().catch((error) => {
93
+ const isDev = process.env.NODE_ENV === "development";
94
+ if (error instanceof Error) {
95
+ console.error(`Error: ${error.message}`);
96
+ if (isDev) {
97
+ console.error(error.stack);
98
+ }
99
+ process.exit(1);
100
+ }
101
+ console.error("An unexpected error occurred.");
102
+ if (isDev) {
103
+ console.error(error);
104
+ }
105
+ process.exit(1);
106
+ });
@@ -0,0 +1,108 @@
1
+ import { Pool } from "pg";
2
+ export const supportedDriversList = ["postgres"];
3
+ async function getTables(pool) {
4
+ const query = `
5
+ SELECT
6
+ table_schema,
7
+ table_name,
8
+ table_type
9
+ FROM information_schema.tables
10
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
11
+ AND table_type = 'BASE TABLE'
12
+ ORDER BY table_schema, table_name;
13
+ `;
14
+ const result = await pool.query(query);
15
+ return result.rows;
16
+ }
17
+ async function getColumns(pool) {
18
+ const query = `
19
+ SELECT
20
+ table_schema,
21
+ table_name,
22
+ column_name,
23
+ ordinal_position,
24
+ udt_name
25
+ FROM information_schema.columns
26
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
27
+ ORDER BY table_schema, table_name, ordinal_position;
28
+ `;
29
+ const result = await pool.query(query);
30
+ return result.rows;
31
+ }
32
+ async function getForeignKeys(pool) {
33
+ const query = `
34
+ SELECT
35
+ tc.constraint_name,
36
+ tc.table_schema AS source_schema,
37
+ tc.table_name AS source_table,
38
+ kcu.column_name AS source_column,
39
+ ccu.table_schema AS target_schema,
40
+ ccu.table_name AS target_table,
41
+ ccu.column_name AS target_column
42
+ FROM information_schema.table_constraints AS tc
43
+ JOIN information_schema.key_column_usage AS kcu
44
+ ON tc.constraint_name = kcu.constraint_name
45
+ AND tc.table_schema = kcu.table_schema
46
+ JOIN information_schema.constraint_column_usage AS ccu
47
+ ON ccu.constraint_name = tc.constraint_name
48
+ AND ccu.table_schema = tc.table_schema
49
+ WHERE tc.constraint_type = 'FOREIGN KEY'
50
+ AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
51
+ ORDER BY
52
+ source_schema,
53
+ source_table,
54
+ tc.constraint_name,
55
+ source_column;
56
+ `;
57
+ const result = await pool.query(query);
58
+ return result.rows;
59
+ }
60
+ function makeTableKey(schema, table) {
61
+ return `${schema}.${table}`;
62
+ }
63
+ export async function getDatabaseInfo(driver, url) {
64
+ switch (driver) {
65
+ case "postgres": {
66
+ const pool = new Pool({ connectionString: url });
67
+ try {
68
+ const [allTables, allColumns, allForeignKeys] = await Promise.all([
69
+ getTables(pool),
70
+ getColumns(pool),
71
+ getForeignKeys(pool),
72
+ ]);
73
+ const columnsByTable = new Map();
74
+ for (const column of allColumns) {
75
+ const key = makeTableKey(column.table_schema, column.table_name);
76
+ const current = columnsByTable.get(key) ?? [];
77
+ current.push({
78
+ name: column.column_name,
79
+ position: Number(column.ordinal_position),
80
+ type: column.udt_name,
81
+ });
82
+ columnsByTable.set(key, current);
83
+ }
84
+ const tables = allTables.map((table) => {
85
+ const key = makeTableKey(table.table_schema, table.table_name);
86
+ return {
87
+ schema: table.table_schema,
88
+ name: table.table_name,
89
+ columns: columnsByTable.get(key) ?? [],
90
+ };
91
+ });
92
+ const relations = allForeignKeys.map((fk) => ({
93
+ name: fk.constraint_name,
94
+ fromSchema: fk.source_schema,
95
+ fromTable: fk.source_table,
96
+ fromColumn: fk.source_column,
97
+ toSchema: fk.target_schema,
98
+ toTable: fk.target_table,
99
+ toColumn: fk.target_column,
100
+ }));
101
+ return { tables, relations };
102
+ }
103
+ finally {
104
+ await pool.end();
105
+ }
106
+ }
107
+ }
108
+ }
@@ -0,0 +1 @@
1
+ export { getDatabaseInfo, supportedDriversList, } from "./core.js";
@@ -0,0 +1 @@
1
+ export { startServer } from "./start-server.js";
@@ -0,0 +1,61 @@
1
+ import * as http from "node:http";
2
+ import { fileURLToPath } from "node:url";
3
+ import path from "node:path";
4
+ import { sendJson, sendResponse, sendText, getContentType, readStaticFile, resolveStaticFilePath, } from "./utils.js";
5
+ export async function startServer(dbInfo, options = {}) {
6
+ const port = options.port ?? 3001;
7
+ const isDev = process.env.NODE_ENV === "development";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const webDir = path.resolve(__dirname, "../web");
11
+ const cachedResources = new Map();
12
+ const server = http.createServer(async (req, res) => {
13
+ const method = req.method ?? "GET";
14
+ const requestUrl = req.url ?? "/";
15
+ if (method !== "GET") {
16
+ sendText(res, 405, "Method Not Allowed");
17
+ return;
18
+ }
19
+ if (requestUrl === "/api/db-info") {
20
+ sendJson(res, 200, dbInfo);
21
+ return;
22
+ }
23
+ if (isDev) {
24
+ sendText(res, 404, "Frontend assets are served by the Vite dev server in development.");
25
+ return;
26
+ }
27
+ const filePath = resolveStaticFilePath(webDir, requestUrl);
28
+ if (filePath === null) {
29
+ sendText(res, 403, "Forbidden");
30
+ return;
31
+ }
32
+ try {
33
+ let file = cachedResources.get(filePath);
34
+ if (!file) {
35
+ file = await readStaticFile(filePath);
36
+ cachedResources.set(filePath, file);
37
+ }
38
+ sendResponse(res, {
39
+ statusCode: 200,
40
+ contentType: getContentType(filePath),
41
+ body: file,
42
+ });
43
+ }
44
+ catch {
45
+ sendText(res, 404, "Not Found");
46
+ }
47
+ });
48
+ await new Promise((resolve, reject) => {
49
+ server.once("error", reject);
50
+ server.listen(port, () => {
51
+ if (isDev) {
52
+ console.log(`API server running at http://localhost:${port}`);
53
+ console.log("Frontend is served by the Vite dev server.");
54
+ }
55
+ else {
56
+ console.log(`View your database graph at http://localhost:${port}`);
57
+ }
58
+ resolve();
59
+ });
60
+ });
61
+ }
@@ -0,0 +1,53 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ export function sendResponse(res, options) {
4
+ const { statusCode, contentType = "text/plain; charset=utf-8", body = "", } = options;
5
+ res.statusCode = statusCode;
6
+ res.setHeader("Content-Type", contentType);
7
+ res.end(body);
8
+ }
9
+ export function sendText(res, statusCode, body = "") {
10
+ sendResponse(res, {
11
+ statusCode,
12
+ contentType: "text/plain; charset=utf-8",
13
+ body,
14
+ });
15
+ }
16
+ export function sendJson(res, statusCode, data) {
17
+ sendResponse(res, {
18
+ statusCode,
19
+ contentType: "application/json; charset=utf-8",
20
+ body: JSON.stringify(data),
21
+ });
22
+ }
23
+ const MIME_TYPES = {
24
+ ".html": "text/html; charset=utf-8",
25
+ ".js": "text/javascript; charset=utf-8",
26
+ ".css": "text/css; charset=utf-8",
27
+ ".json": "application/json; charset=utf-8",
28
+ ".svg": "image/svg+xml",
29
+ ".png": "image/png",
30
+ ".jpg": "image/jpeg",
31
+ ".jpeg": "image/jpeg",
32
+ ".webp": "image/webp",
33
+ ".ico": "image/x-icon",
34
+ };
35
+ export function getContentType(filePath) {
36
+ const extension = path.extname(filePath).toLowerCase();
37
+ return MIME_TYPES[extension] ?? "application/octet-stream";
38
+ }
39
+ export function getPathname(requestUrl) {
40
+ return decodeURIComponent(requestUrl.split("?")[0] ?? "/");
41
+ }
42
+ export function resolveStaticFilePath(rootDir, requestUrl) {
43
+ const pathname = decodeURIComponent(requestUrl.split("?")[0] ?? "/");
44
+ const relativePath = pathname === "/" ? "index.html" : pathname.replace(/^\/+/, "");
45
+ const filePath = path.resolve(rootDir, relativePath);
46
+ if (!filePath.startsWith(rootDir)) {
47
+ return null;
48
+ }
49
+ return filePath;
50
+ }
51
+ export async function readStaticFile(filePath) {
52
+ return fs.readFile(filePath);
53
+ }
@@ -0,0 +1 @@
1
+ html,body,#root{width:100%;height:100%;margin:0;font-family:Inter,system-ui,sans-serif}.react-flow{--xy-edge-stroke-default:#b1b1b7;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#555;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#ffffff80;--xy-minimap-background-color-default:#fff;--xy-minimap-mask-background-color-default:#f0f0f099;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#e2e2e2;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:transparent;--xy-background-pattern-dots-color-default:#91919a;--xy-background-pattern-lines-color-default:#eee;--xy-background-pattern-cross-color-default:#e2e2e2;background-color:var(--xy-background-color,var(--xy-background-color-default));--xy-node-color-default:inherit;--xy-node-border-default:1px solid #1a192b;--xy-node-background-color-default:#fff;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #00000014;--xy-node-boxshadow-selected-default:0 0 0 .5px #1a192b;--xy-node-border-radius-default:3px;--xy-handle-background-color-default:#1a192b;--xy-handle-border-color-default:#fff;--xy-selection-background-color-default:#0059dc14;--xy-selection-border-default:1px dotted #0059dccc;--xy-controls-button-background-color-default:#fefefe;--xy-controls-button-background-color-hover-default:#f4f4f4;--xy-controls-button-color-default:inherit;--xy-controls-button-color-hover-default:inherit;--xy-controls-button-border-color-default:#eee;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#fff;--xy-edge-label-color-default:inherit;--xy-resize-background-color-default:#3367d9;direction:ltr}.react-flow.dark{--xy-edge-stroke-default:#3e3e3e;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#727272;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#96969640;--xy-minimap-background-color-default:#141414;--xy-minimap-mask-background-color-default:#3c3c3c99;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#2b2b2b;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:#141414;--xy-background-pattern-dots-color-default:#777;--xy-background-pattern-lines-color-default:#777;--xy-background-pattern-cross-color-default:#777;--xy-node-color-default:#f8f8f8;--xy-node-border-default:1px solid #3c3c3c;--xy-node-background-color-default:#1e1e1e;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #ffffff14;--xy-node-boxshadow-selected-default:0 0 0 .5px #999;--xy-handle-background-color-default:#bebebe;--xy-handle-border-color-default:#1e1e1e;--xy-selection-background-color-default:#c8c8dc14;--xy-selection-border-default:1px dotted #c8c8dccc;--xy-controls-button-background-color-default:#2b2b2b;--xy-controls-button-background-color-hover-default:#3e3e3e;--xy-controls-button-color-default:#f8f8f8;--xy-controls-button-color-hover-default:#fff;--xy-controls-button-border-color-default:#5b5b5b;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#141414;--xy-edge-label-color-default:#f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props,var(--xy-background-color,var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width,var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke,var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width,var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{pointer-events:none;position:absolute;overflow:visible}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:.5s linear infinite dashdraw}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected,var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:.5s linear infinite dashdraw}svg.react-flow__connectionline{z-index:1001;position:absolute;overflow:visible}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{-webkit-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default;position:absolute}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:0 0;pointer-events:none}.react-flow__nodesselection-rect{pointer-events:all;cursor:grab;position:absolute}.react-flow__handle{pointer-events:none;background-color:var(--xy-handle-background-color,var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color,var(--xy-handle-border-color-default));border-radius:100%;width:6px;min-width:5px;height:6px;min-height:5px;position:absolute}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;bottom:0;left:50%;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{z-index:5;margin:15px;position:absolute}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px)translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px)translateY(-50%)}.react-flow__attribution{background:var(--xy-attribution-background-color,var(--xy-attribution-background-color-default));margin:0;padding:2px 3px;font-size:10px}.react-flow__attribution a{color:#999;text-decoration:none}@keyframes dashdraw{0%{stroke-dashoffset:10px}}.react-flow__edgelabel-renderer{pointer-events:none;-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__viewport-portal{-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__minimap{background:var(--xy-minimap-background-color-props,var(--xy-minimap-background-color,var(--xy-minimap-background-color-default)))}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var(--xy-minimap-mask-background-color-props,var(--xy-minimap-mask-background-color,var(--xy-minimap-mask-background-color-default)));stroke:var(--xy-minimap-mask-stroke-color-props,var(--xy-minimap-mask-stroke-color,var(--xy-minimap-mask-stroke-color-default)));stroke-width:var(--xy-minimap-mask-stroke-width-props,var(--xy-minimap-mask-stroke-width,var(--xy-minimap-mask-stroke-width-default)))}.react-flow__minimap-node{fill:var(--xy-minimap-node-background-color-props,var(--xy-minimap-node-background-color,var(--xy-minimap-node-background-color-default)));stroke:var(--xy-minimap-node-stroke-color-props,var(--xy-minimap-node-stroke-color,var(--xy-minimap-node-stroke-color-default)));stroke-width:var(--xy-minimap-node-stroke-width-props,var(--xy-minimap-node-stroke-width,var(--xy-minimap-node-stroke-width-default)))}.react-flow__background-pattern.dots{fill:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-dots-color-default)))}.react-flow__background-pattern.lines{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-lines-color-default)))}.react-flow__background-pattern.cross{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-cross-color-default)))}.react-flow__controls{box-shadow:var(--xy-controls-box-shadow,var(--xy-controls-box-shadow-default));flex-direction:column;display:flex}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{background:var(--xy-controls-button-background-color,var(--xy-controls-button-background-color-default));border:none;border-bottom:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)));width:26px;height:26px;color:var(--xy-controls-button-color-props,var(--xy-controls-button-color,var(--xy-controls-button-color-default)));cursor:pointer;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;padding:4px;display:flex}.react-flow__controls-button svg{fill:currentColor;width:100%;max-width:12px;max-height:12px}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{border-radius:var(--xy-node-border-radius,var(--xy-node-border-radius-default));width:150px;color:var(--xy-node-color,var(--xy-node-color-default));text-align:center;border:var(--xy-node-border,var(--xy-node-border-default));background-color:var(--xy-node-background-color,var(--xy-node-background-color-default));padding:10px;font-size:12px}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover,var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected,var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color,var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color,var(--xy-selection-background-color-default));border:var(--xy-selection-border,var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var(--xy-controls-button-background-color-hover-props,var(--xy-controls-button-background-color-hover,var(--xy-controls-button-background-color-hover-default)));color:var(--xy-controls-button-color-hover-props,var(--xy-controls-button-color-hover,var(--xy-controls-button-color-hover-default)))}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)))}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{background-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border:1px solid #fff;border-radius:1px;width:5px;height:5px;translate:-50% -50%}.react-flow__resize-control.handle.left{top:50%;left:0}.react-flow__resize-control.handle.right{top:50%;left:100%}.react-flow__resize-control.handle.top{top:0;left:50%}.react-flow__resize-control.handle.bottom{top:100%;left:50%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border-style:solid;border-width:0}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;height:100%;top:0;transform:translate(-50%)}.react-flow__resize-control.line.left{border-left-width:1px;left:0}.react-flow__resize-control.line.right{border-right-width:1px;left:100%}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{width:100%;height:1px;left:0;transform:translateY(-50%)}.react-flow__resize-control.line.top{border-top-width:1px;top:0}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color,var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color,var(--xy-edge-label-color-default))}