@doc-karta/karta 0.1.1

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,133 @@
1
+ /**
2
+ * doc-karta dev server. Serves the prebuilt UI (`dist/`), runs `karta-core
3
+ * watch` against the target repo, and pushes registry updates to the browser
4
+ * over a WebSocket so the graph hot-reloads with no refresh — the consumer-side
5
+ * equivalent of the Vite-plugin live reload used when developing doc-karta.
6
+ *
7
+ * Configured by env (set by `bin/karta.js dev`):
8
+ * KARTA_DIST_DIR directory of the built UI (default: ../dist)
9
+ * KARTA_WATCH_DIR repo to crawl + watch (default: the bundled fixture)
10
+ * PORT listen port (default: 4317)
11
+ */
12
+ import { spawn } from 'node:child_process'
13
+ import fs from 'node:fs'
14
+ import path from 'node:path'
15
+ import { createInterface } from 'node:readline'
16
+ import { node } from '@elysiajs/node'
17
+ import { Elysia } from 'elysia'
18
+ import { resolveCore } from './resolve-core.js'
19
+
20
+ const PORT = Number(process.env.PORT) || 4317
21
+ const distDir = process.env.KARTA_DIST_DIR
22
+ ? path.resolve(process.env.KARTA_DIST_DIR)
23
+ : path.resolve(import.meta.dirname, '../dist')
24
+ const watchDir = process.env.KARTA_WATCH_DIR
25
+ ? path.resolve(process.env.KARTA_WATCH_DIR)
26
+ : path.resolve(import.meta.dirname, '../../..', 'fixtures/sample-repo')
27
+
28
+ if (!fs.existsSync(path.join(distDir, 'index.html'))) {
29
+ console.error(`[karta] no built UI at ${distDir} — run \`pnpm --filter karta build\` first.`)
30
+ process.exit(1)
31
+ }
32
+
33
+ /** Freshest crawl, shared by the HTTP route and pushed over the socket. */
34
+ let latest = null
35
+ /** Connected live-reload sockets. */
36
+ const sockets = new Set()
37
+
38
+ const CONTENT_TYPES = {
39
+ '.html': 'text/html; charset=utf-8',
40
+ '.js': 'text/javascript; charset=utf-8',
41
+ '.css': 'text/css; charset=utf-8',
42
+ '.json': 'application/json',
43
+ '.svg': 'image/svg+xml',
44
+ '.map': 'application/json',
45
+ }
46
+
47
+ // Tell the client it's served live (so it opens the WS). Static hosts omit this
48
+ // marker and the client stays inert.
49
+ const LIVE_MARKER = '<script>window.__KARTA_LIVE__=true</script>'
50
+
51
+ /** Serve a file from dist, injecting the live marker into HTML. Null if absent. */
52
+ function serveStatic(urlPath, set) {
53
+ const rel = urlPath === '/' ? 'index.html' : urlPath.replace(/^\/+/, '')
54
+ const abs = path.join(distDir, rel)
55
+ // Stay inside dist.
56
+ if (!abs.startsWith(distDir) || !fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
57
+ return null
58
+ }
59
+ const ext = path.extname(abs)
60
+ set.headers['content-type'] = CONTENT_TYPES[ext] ?? 'application/octet-stream'
61
+ if (ext === '.html') {
62
+ return fs.readFileSync(abs, 'utf8').replace('</head>', `${LIVE_MARKER}</head>`)
63
+ }
64
+ return fs.readFileSync(abs)
65
+ }
66
+
67
+ const app = new Elysia({ adapter: node() })
68
+ .get('/health', () => ({ ok: true, service: 'karta' }))
69
+ .get('/registry.json', ({ set }) => {
70
+ set.headers['content-type'] = 'application/json'
71
+ if (latest) return latest
72
+ const fallback = path.join(distDir, 'registry.json')
73
+ return fs.existsSync(fallback)
74
+ ? fs.readFileSync(fallback, 'utf8')
75
+ : '{"version":"1.0","nodes":[],"edges":[]}'
76
+ })
77
+ .ws('/__karta_live', {
78
+ open(ws) {
79
+ sockets.add(ws)
80
+ },
81
+ close(ws) {
82
+ sockets.delete(ws)
83
+ },
84
+ })
85
+ // Static UI + SPA fallback to index.html for extensionless routes.
86
+ .get('*', ({ path: urlPath, set }) => {
87
+ const file = serveStatic(urlPath, set)
88
+ if (file !== null) return file
89
+ if (!path.extname(urlPath)) return serveStatic('/', set)
90
+ set.status = 404
91
+ return 'Not found'
92
+ })
93
+
94
+ // --- watcher: stream NDJSON registry updates, broadcast to sockets ----------
95
+ const core = resolveCore()
96
+ const child = spawn(core.cmd, [...core.args, 'watch', watchDir], { env: process.env })
97
+
98
+ child.on('error', (err) => {
99
+ console.error(`[karta] failed to start watcher (mode: ${core.mode}): ${err.message}`)
100
+ })
101
+ child.stderr?.on('data', (chunk) => process.stderr.write(chunk))
102
+
103
+ if (child.stdout) {
104
+ const rl = createInterface({ input: child.stdout })
105
+ rl.on('line', (line) => {
106
+ if (!line.trim()) return
107
+ let msg
108
+ try {
109
+ msg = JSON.parse(line)
110
+ } catch {
111
+ return
112
+ }
113
+ if (msg.ok && msg.registry) {
114
+ latest = JSON.stringify(msg.registry)
115
+ console.log(
116
+ `[karta] registry updated — ${msg.registry.nodes.length} nodes, ${msg.registry.edges.length} edges`,
117
+ )
118
+ } else {
119
+ console.warn(`[karta] crawl failed:\n${msg.error}`)
120
+ }
121
+ for (const ws of sockets) ws.send(line)
122
+ })
123
+ }
124
+
125
+ const stop = () => child.kill()
126
+ process.once('exit', stop)
127
+ process.once('SIGINT', () => process.exit(0))
128
+ process.once('SIGTERM', () => process.exit(0))
129
+
130
+ app.listen(PORT, () => {
131
+ console.log(`\n doc-karta → http://localhost:${PORT}`)
132
+ console.log(` watching ${watchDir}\n`)
133
+ })
package/bin/karta.js ADDED
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point for `npx karta`.
4
+ *
5
+ * init Scaffold .docviz/config.ts + a runnable example (zero-config).
6
+ * dev Serve the UI + watch the repo; the graph hot-reloads on save.
7
+ * build Crawl the repo into a self-contained static site.
8
+ * scan Fail if any doc contains a likely secret (also gates build).
9
+ */
10
+ import { spawn, spawnSync } from 'node:child_process'
11
+ import fs from 'node:fs'
12
+ import path from 'node:path'
13
+ import { resolveCore } from './resolve-core.js'
14
+
15
+ const cmd = process.argv[2] ?? 'help'
16
+
17
+ const help = `karta <command> [dir]
18
+
19
+ Commands:
20
+ init Scaffold .docviz/config.ts + a runnable example in this project
21
+ dev Serve the UI and watch [dir] (default: .) — graph hot-reloads on save
22
+ build Crawl [dir] (default: .) into a static site (default out: karta-dist)
23
+ scan Scan [dir] (default: .) for likely secrets — exits non-zero if found
24
+ help Show this message
25
+
26
+ For local development of the UI shell, run:
27
+ pnpm --filter karta dev
28
+ `
29
+
30
+ // --- init --------------------------------------------------------------------
31
+
32
+ /** Derive a project namespace from the target dir's package.json, or its name. */
33
+ function inferProject(cwd) {
34
+ try {
35
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
36
+ if (typeof pkg.name === 'string' && pkg.name) {
37
+ // Strip an `@scope/` prefix; lowercase for the id convention.
38
+ return pkg.name.replace(/^@[^/]+\//, '').toLowerCase()
39
+ }
40
+ } catch {
41
+ // No (readable) package.json — fall back to the directory name.
42
+ }
43
+ return path.basename(cwd).toLowerCase()
44
+ }
45
+
46
+ /** Find existing `*.mdx` files that live under a `docs/` folder (PER-9). */
47
+ function findExistingDocs(cwd) {
48
+ const ignore = new Set(['node_modules', 'dist', 'target', 'karta-dist', 'build'])
49
+ const out = []
50
+ const walk = (dir, depth) => {
51
+ if (depth > 7 || out.length > 50) return
52
+ let entries
53
+ try {
54
+ entries = fs.readdirSync(dir, { withFileTypes: true })
55
+ } catch {
56
+ return
57
+ }
58
+ for (const e of entries) {
59
+ const full = path.join(dir, e.name)
60
+ if (e.isDirectory()) {
61
+ if (ignore.has(e.name) || e.name.startsWith('.')) continue
62
+ walk(full, depth + 1)
63
+ } else if (e.name.endsWith('.mdx')) {
64
+ const rel = path.relative(cwd, full)
65
+ if (rel.split(path.sep).includes('docs')) out.push(rel)
66
+ }
67
+ }
68
+ }
69
+ walk(cwd, 0)
70
+ return out
71
+ }
72
+
73
+ /**
74
+ * Decide `include` globs so the first crawl is never empty (PER-9):
75
+ * - existing docs → narrow globs to their top-level roots (faster on big repos),
76
+ * and skip the example;
77
+ * - none → universal `**​/docs/**​/*.mdx` + scaffold the example.
78
+ */
79
+ function inferInclude(cwd) {
80
+ const existing = findExistingDocs(cwd)
81
+ if (existing.length) {
82
+ const tops = new Set(existing.map((rel) => rel.split(path.sep)[0]))
83
+ const include = tops.has('docs')
84
+ ? ['**/docs/**/*.mdx']
85
+ : [...tops].map((t) => `${t}/**/docs/**/*.mdx`)
86
+ return {
87
+ include,
88
+ scaffoldExample: false,
89
+ note: `found ${existing.length} existing doc(s) → ${include.join(', ')}`,
90
+ }
91
+ }
92
+ const markers = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'].filter((m) =>
93
+ fs.existsSync(path.join(cwd, m)),
94
+ )
95
+ const note = markers.length ? `monorepo detected (${markers.join(', ')})` : null
96
+ return { include: ['**/docs/**/*.mdx'], scaffoldExample: true, note }
97
+ }
98
+
99
+ const configTs = (project, include) => `interface DocvizConfig {
100
+ project: string
101
+ include: string[]
102
+ exclude?: string[]
103
+ environments?: Record<string, string>
104
+ }
105
+
106
+ export default {
107
+ project: '${project}',
108
+ // Globs (relative to this repo's root) locating your MDX docs.
109
+ include: [${include.map((g) => `'${g}'`).join(', ')}],
110
+ } satisfies DocvizConfig
111
+ `
112
+
113
+ const welcomePage = `---
114
+ docviz_version: "1.0"
115
+ id: welcome-page
116
+ type: page
117
+ title: Welcome
118
+ status: draft
119
+ audience: [tech]
120
+ renders:
121
+ - welcome-card
122
+ ---
123
+
124
+ # Welcome
125
+
126
+ Your first doc-karta page. It renders the Welcome card. Edit this file (or add a
127
+ \`calls:\` to a BFF) and the graph hot-reloads. Delete the \`example/\` folder once
128
+ you've authored your own docs.
129
+ `
130
+
131
+ const welcomeCard = `---
132
+ docviz_version: "1.0"
133
+ id: welcome-card
134
+ type: component
135
+ title: Welcome Card
136
+ status: draft
137
+ audience: [tech]
138
+ ---
139
+
140
+ # Welcome Card
141
+
142
+ A component rendered by the Welcome page. Components are the leaves of the graph.
143
+ `
144
+
145
+ /** Write `content` to `rel` under `cwd` unless it exists; report either way. */
146
+ function scaffoldFile(cwd, rel, content) {
147
+ const abs = path.join(cwd, rel)
148
+ if (fs.existsSync(abs)) {
149
+ process.stdout.write(` • skipped ${rel} (exists)\n`)
150
+ return
151
+ }
152
+ fs.mkdirSync(path.dirname(abs), { recursive: true })
153
+ fs.writeFileSync(abs, content)
154
+ process.stdout.write(` ✓ created ${rel}\n`)
155
+ }
156
+
157
+ function init() {
158
+ const cwd = process.cwd()
159
+ const project = inferProject(cwd)
160
+ const { include, scaffoldExample, note } = inferInclude(cwd)
161
+ process.stdout.write(`Scaffolding doc-karta (project: ${project})\n`)
162
+ if (note) process.stdout.write(` ${note}\n`)
163
+ scaffoldFile(cwd, '.docviz/config.ts', configTs(project, include))
164
+ if (scaffoldExample) {
165
+ scaffoldFile(cwd, 'example/docs/welcome-page.mdx', welcomePage)
166
+ scaffoldFile(cwd, 'example/docs/welcome-card.mdx', welcomeCard)
167
+ }
168
+ process.stdout.write('\nNext: npx karta dev (in this repo: pnpm --filter karta dev)\n')
169
+ }
170
+
171
+ // --- dev ---------------------------------------------------------------------
172
+
173
+ function dev() {
174
+ const dir = path.resolve(process.argv[3] ?? '.')
175
+ const server = path.join(import.meta.dirname, 'dev-server.js')
176
+ const child = spawn(process.execPath, [server], {
177
+ stdio: 'inherit',
178
+ env: { ...process.env, KARTA_WATCH_DIR: dir },
179
+ })
180
+ child.on('exit', (code) => process.exit(code ?? 0))
181
+ }
182
+
183
+ // --- scan --------------------------------------------------------------------
184
+
185
+ /** Scan `dir` for secrets via the core; returns the child exit status. */
186
+ function runScan(dir) {
187
+ const core = resolveCore()
188
+ const res = spawnSync(core.cmd, [...core.args, 'scan', dir], { encoding: 'utf8' })
189
+ if (res.stdout) process.stdout.write(res.stdout)
190
+ if (res.stderr) process.stderr.write(res.stderr)
191
+ return res.status ?? 1
192
+ }
193
+
194
+ function scan() {
195
+ const dir = path.resolve(process.argv[3] ?? '.')
196
+ process.exit(runScan(dir))
197
+ }
198
+
199
+ // --- build -------------------------------------------------------------------
200
+
201
+ function build() {
202
+ const dir = path.resolve(process.argv[3] ?? '.')
203
+ const out = path.resolve(process.argv[4] ?? 'karta-dist')
204
+ const distDir = path.resolve(import.meta.dirname, '../dist')
205
+ if (!fs.existsSync(path.join(distDir, 'index.html'))) {
206
+ process.stderr.write('karta: built UI not found — run `pnpm --filter karta build` first.\n')
207
+ process.exit(1)
208
+ }
209
+
210
+ // Secrets are a build error in stakeholder-facing output — gate before crawl.
211
+ if (runScan(dir) !== 0) process.exit(1)
212
+
213
+ const core = resolveCore()
214
+ const res = spawnSync(core.cmd, [...core.args, 'crawl', dir], { encoding: 'utf8' })
215
+ if (res.status !== 0) {
216
+ // Broken reference / bad type is a build error — pass it through.
217
+ process.stderr.write(res.stderr || 'karta: crawl failed\n')
218
+ process.exit(1)
219
+ }
220
+
221
+ fs.cpSync(distDir, out, { recursive: true })
222
+ fs.writeFileSync(path.join(out, 'registry.json'), res.stdout)
223
+
224
+ const reg = JSON.parse(res.stdout)
225
+ const rel = path.relative(process.cwd(), out) || out
226
+ process.stdout.write(`✓ built ${rel} — ${reg.nodes.length} nodes, ${reg.edges.length} edges\n`)
227
+ process.stdout.write(` serve it: npx serve ${rel}\n`)
228
+ }
229
+
230
+ // --- dispatch ----------------------------------------------------------------
231
+
232
+ switch (cmd) {
233
+ case 'init':
234
+ init()
235
+ break
236
+ case 'dev':
237
+ dev()
238
+ break
239
+ case 'build':
240
+ build()
241
+ break
242
+ case 'scan':
243
+ scan()
244
+ break
245
+ case 'help':
246
+ case '--help':
247
+ case '-h':
248
+ process.stdout.write(help)
249
+ break
250
+ default:
251
+ process.stdout.write(`karta: "${cmd}" is not a command.\n\n${help}`)
252
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-install hint. Prints a single line nudging the user to scaffold their
4
+ * project with `karta init`. It writes nothing — scaffolding is explicit
5
+ * and idempotent (see bin/karta.js `init`), never an install-time side effect.
6
+ *
7
+ * No-op outside a dependency install (e.g. local dev in this monorepo), so
8
+ * `pnpm install` here stays quiet.
9
+ */
10
+ if (import.meta.dirname.includes('node_modules')) {
11
+ process.stdout.write(
12
+ '\n👋 doc-karta installed — run `karta init` to scaffold .docviz/config.ts and a runnable example.\n\n',
13
+ )
14
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Locate the `karta-core` binary. Single source of truth for `dev`, `build`,
3
+ * and the Elysia dev server.
4
+ *
5
+ * Resolution order:
6
+ * 1. The platform-specific optional dependency `@doc-karta/core-<platform>-<arch>`
7
+ * (shipped prebuilt, esbuild-style — no postinstall download).
8
+ * 2. Dev fallback inside this monorepo: `target/release|debug/karta-core`.
9
+ * 3. Last resort: `cargo run -q -p karta-core --` (requires a Rust toolchain).
10
+ *
11
+ * Returns `{ cmd, args, mode }` so callers spawn uniformly:
12
+ * spawn(cmd, [...args, ...subcommandArgs])
13
+ */
14
+
15
+ import fs from 'node:fs'
16
+ import { createRequire } from 'node:module'
17
+ import path from 'node:path'
18
+
19
+ const require = createRequire(import.meta.url)
20
+
21
+ // packages/karta/bin → repo root is three levels up.
22
+ const REPO_ROOT = path.resolve(import.meta.dirname, '../../..')
23
+
24
+ /** npm package + binary file name for the current platform. */
25
+ function platformTarget() {
26
+ const ext = process.platform === 'win32' ? '.exe' : ''
27
+ return {
28
+ pkg: `@doc-karta/core-${process.platform}-${process.arch}`,
29
+ bin: `karta-core${ext}`,
30
+ }
31
+ }
32
+
33
+ export function resolveCore() {
34
+ const { pkg, bin } = platformTarget()
35
+
36
+ // 1. Prebuilt platform package.
37
+ try {
38
+ const binPath = require.resolve(`${pkg}/${bin}`)
39
+ return { cmd: binPath, args: [], mode: 'prebuilt' }
40
+ } catch {
41
+ // Not installed (e.g. running from source) — fall through.
42
+ }
43
+
44
+ // 2. Locally-built binary (monorepo dev).
45
+ for (const profile of ['release', 'debug']) {
46
+ const local = path.join(REPO_ROOT, 'target', profile, bin)
47
+ if (fs.existsSync(local)) {
48
+ return { cmd: local, args: [], mode: profile }
49
+ }
50
+ }
51
+
52
+ // 3. Build-and-run via cargo.
53
+ return { cmd: 'cargo', args: ['run', '-q', '-p', 'karta-core', '--'], mode: 'cargo' }
54
+ }
@@ -0,0 +1 @@
1
+ .react-flow{direction:ltr;--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: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--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: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--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: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--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 rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.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: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--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: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--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: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--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 rgba(0, 0, 0, .08);--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{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.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{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.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;-moz-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:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.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:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;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%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;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{position:absolute;z-index:5;margin:15px}.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{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.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{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.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{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;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))}.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{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.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-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.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))}