visual-node 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.
Files changed (56) hide show
  1. package/README.md +42 -0
  2. package/dist/app.d.ts +3 -0
  3. package/dist/app.js +44 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/codegen-helpers.d.ts +55 -0
  6. package/dist/codegen-helpers.js +142 -0
  7. package/dist/codegen-helpers.js.map +1 -0
  8. package/dist/config.d.ts +9 -0
  9. package/dist/config.js +13 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/connect/compile-function-graph.service.d.ts +14 -0
  12. package/dist/connect/compile-function-graph.service.js +106 -0
  13. package/dist/connect/compile-function-graph.service.js.map +1 -0
  14. package/dist/connect/files.service.d.ts +7 -0
  15. package/dist/connect/files.service.js +175 -0
  16. package/dist/connect/files.service.js.map +1 -0
  17. package/dist/connect/node-registry-flow.service.d.ts +14 -0
  18. package/dist/connect/node-registry-flow.service.js +142 -0
  19. package/dist/connect/node-registry-flow.service.js.map +1 -0
  20. package/dist/connect/plugins.service.d.ts +24 -0
  21. package/dist/connect/plugins.service.js +73 -0
  22. package/dist/connect/plugins.service.js.map +1 -0
  23. package/dist/connect/run.service.d.ts +3 -0
  24. package/dist/connect/run.service.js +159 -0
  25. package/dist/connect/run.service.js.map +1 -0
  26. package/dist/connect/validate-generate.service.d.ts +19 -0
  27. package/dist/connect/validate-generate.service.js +102 -0
  28. package/dist/connect/validate-generate.service.js.map +1 -0
  29. package/dist/file-tree.d.ts +23 -0
  30. package/dist/file-tree.js +63 -0
  31. package/dist/file-tree.js.map +1 -0
  32. package/dist/flow-shape.d.ts +8 -0
  33. package/dist/flow-shape.js +13 -0
  34. package/dist/flow-shape.js.map +1 -0
  35. package/dist/path-safety.d.ts +12 -0
  36. package/dist/path-safety.js +26 -0
  37. package/dist/path-safety.js.map +1 -0
  38. package/dist/plugin-loading.d.ts +18 -0
  39. package/dist/plugin-loading.js +47 -0
  40. package/dist/plugin-loading.js.map +1 -0
  41. package/dist/plugin-readme.d.ts +9 -0
  42. package/dist/plugin-readme.js +235 -0
  43. package/dist/plugin-readme.js.map +1 -0
  44. package/dist/public/assets/index-6_2vDtnd.css +1 -0
  45. package/dist/public/assets/index-BpXY8lVq.js +101 -0
  46. package/dist/public/index.html +13 -0
  47. package/dist/runner.d.ts +23 -0
  48. package/dist/runner.js +65 -0
  49. package/dist/runner.js.map +1 -0
  50. package/dist/server.d.ts +2 -0
  51. package/dist/server.js +34 -0
  52. package/dist/server.js.map +1 -0
  53. package/dist/static.d.ts +12 -0
  54. package/dist/static.js +28 -0
  55. package/dist/static.js.map +1 -0
  56. package/package.json +39 -0
@@ -0,0 +1,63 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const IGNORED = new Set(["node_modules", ".git"]);
4
+ /**
5
+ * Lists the whole project directory as a tree, excluding node_modules/.git/dotfiles.
6
+ * Shows *all* remaining files (not just `.blueprint`) so the tree reflects the real
7
+ * directory — the UI only treats `.blueprint` files as openable. Folders sort before
8
+ * files, alphabetical within each group.
9
+ */
10
+ export async function listTree(projectDir) {
11
+ return listDir(projectDir, "");
12
+ }
13
+ async function listDir(absoluteDir, relativeDir) {
14
+ let entries;
15
+ try {
16
+ entries = await readdir(absoluteDir, { withFileTypes: true });
17
+ }
18
+ catch (err) {
19
+ if (err.code === "ENOENT")
20
+ return [];
21
+ throw err;
22
+ }
23
+ const nodes = [];
24
+ for (const entry of entries) {
25
+ if (entry.name.startsWith(".") || IGNORED.has(entry.name))
26
+ continue;
27
+ // Built manually with "/" (not path.join) so relativePath stays POSIX-style even on
28
+ // Windows, matching core's compileProject path contract.
29
+ const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
30
+ if (entry.isDirectory()) {
31
+ nodes.push({
32
+ type: "folder",
33
+ name: entry.name,
34
+ relativePath,
35
+ children: await listDir(path.join(absoluteDir, entry.name), relativePath),
36
+ });
37
+ }
38
+ else {
39
+ nodes.push({ type: "file", name: entry.name, relativePath });
40
+ }
41
+ }
42
+ nodes.sort((a, b) => {
43
+ if (a.type !== b.type)
44
+ return a.type === "folder" ? -1 : 1;
45
+ return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
46
+ });
47
+ return nodes;
48
+ }
49
+ /** Flat list of every ".blueprint" file's relative path, for /api/compile. */
50
+ export async function listBlueprintFiles(projectDir) {
51
+ const results = [];
52
+ const walk = (nodes) => {
53
+ for (const n of nodes) {
54
+ if (n.type === "file" && n.name.endsWith(".blueprint"))
55
+ results.push({ relativePath: n.relativePath });
56
+ if (n.type === "folder")
57
+ walk(n.children);
58
+ }
59
+ };
60
+ walk(await listTree(projectDir));
61
+ return results;
62
+ }
63
+ //# sourceMappingURL=file-tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-tree.js","sourceRoot":"","sources":["../src/file-tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,IAAI,MAAM,WAAW,CAAC;AAiB7B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC;AAElD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,UAAkB;IAC/C,OAAO,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,WAAmB,EAAE,WAAmB;IAC7D,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QAChE,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,SAAS;QACpE,oFAAoF;QACpF,yDAAyD;QACzD,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QAC/E,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,YAAY;gBACZ,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;aAC1E,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAClB,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;YAAE,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,OAAO,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,UAAkB;IACzD,MAAM,OAAO,GAA+B,EAAE,CAAC;IAC/C,MAAM,IAAI,GAAG,CAAC,KAAqB,EAAE,EAAE;QACrC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC;YACvG,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;gBAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAC,MAAM,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;IACjC,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Flow } from "@visual-node/core";
2
+ /**
3
+ * Shallow shape check for a client-supplied "flow" request body — just enough to catch
4
+ * garbage before it hits the filesystem or core's real `validateFlow()`. Shared between
5
+ * `flow.routes.ts` (the single fixed flow.json) and `files.routes.ts` (per-file blueprints)
6
+ * since both accept a `Flow`-shaped body from the client.
7
+ */
8
+ export declare function isPlausibleFlow(value: unknown): value is Flow;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shallow shape check for a client-supplied "flow" request body — just enough to catch
3
+ * garbage before it hits the filesystem or core's real `validateFlow()`. Shared between
4
+ * `flow.routes.ts` (the single fixed flow.json) and `files.routes.ts` (per-file blueprints)
5
+ * since both accept a `Flow`-shaped body from the client.
6
+ */
7
+ export function isPlausibleFlow(value) {
8
+ if (!value || typeof value !== "object")
9
+ return false;
10
+ const flow = value;
11
+ return Array.isArray(flow.nodes) && Array.isArray(flow.edges) && typeof flow.meta === "object" && flow.meta !== null;
12
+ }
13
+ //# sourceMappingURL=flow-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flow-shape.js","sourceRoot":"","sources":["../src/flow-shape.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,IAAI,GAAG,KAAgC,CAAC;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;AACvH,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolves a client-supplied, project-relative path to an absolute filesystem path,
3
+ * refusing anything that could escape `projectDir`. This is the first place in the
4
+ * codebase that accepts a client-supplied path (every existing route only ever builds
5
+ * paths from fixed literals), so real protection is required here.
6
+ *
7
+ * Rejects `..` segments and absolute paths (POSIX or Windows-drive-letter) outright, then
8
+ * re-verifies the resolved path is still inside `projectDir` as defense in depth. Symlink
9
+ * resolution (`fs.realpath`) is deliberately skipped — this is a single-user local tool
10
+ * with one trusted project directory, not a multi-tenant service.
11
+ */
12
+ export declare function resolveSafePath(projectDir: string, relativePath: string): string | null;
@@ -0,0 +1,26 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Resolves a client-supplied, project-relative path to an absolute filesystem path,
4
+ * refusing anything that could escape `projectDir`. This is the first place in the
5
+ * codebase that accepts a client-supplied path (every existing route only ever builds
6
+ * paths from fixed literals), so real protection is required here.
7
+ *
8
+ * Rejects `..` segments and absolute paths (POSIX or Windows-drive-letter) outright, then
9
+ * re-verifies the resolved path is still inside `projectDir` as defense in depth. Symlink
10
+ * resolution (`fs.realpath`) is deliberately skipped — this is a single-user local tool
11
+ * with one trusted project directory, not a multi-tenant service.
12
+ */
13
+ export function resolveSafePath(projectDir, relativePath) {
14
+ if (typeof relativePath !== "string" || relativePath.length === 0)
15
+ return null;
16
+ if (path.isAbsolute(relativePath) || /^[a-zA-Z]:/.test(relativePath))
17
+ return null;
18
+ if (relativePath.split(/[\\/]/).some((seg) => seg === ".."))
19
+ return null;
20
+ const root = path.resolve(projectDir);
21
+ const resolved = path.resolve(root, relativePath);
22
+ if (resolved !== root && !resolved.startsWith(root + path.sep))
23
+ return null;
24
+ return resolved;
25
+ }
26
+ //# sourceMappingURL=path-safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-safety.js","sourceRoot":"","sources":["../src/path-safety.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,YAAoB;IACtE,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/E,IAAI,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IAClF,IAAI,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACzE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAClD,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5E,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,18 @@
1
+ export interface LoadInstalledPluginsResult {
2
+ /** `type` of each plugin successfully (re-)registered. */
3
+ loaded: string[];
4
+ /** One entry per `.node.json` file that failed to parse/validate — the rest are still processed. */
5
+ failed: {
6
+ file: string;
7
+ error: string;
8
+ }[];
9
+ }
10
+ /**
11
+ * Re-registers every previously-installed plugin node (Phase 9 Part B) on startup, from
12
+ * `<projectDir>/.flowserver/plugins/*.node.json` — mirrors `registerBuiltinNodes()`'s own
13
+ * "skip if already registered" idempotency guard (see packages/core/src/nodes/index.ts), so
14
+ * this stays safe to call more than once. A missing `.flowserver/plugins` directory (no
15
+ * plugins ever installed) is treated as "nothing to load", not an error. A single malformed
16
+ * or invalid file is recorded in `failed` and does not abort loading the rest.
17
+ */
18
+ export declare function loadInstalledPlugins(projectDir: string): Promise<LoadInstalledPluginsResult>;
@@ -0,0 +1,47 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createPluginNodeDefinition, getNodeDefinition, registerNode, validatePluginNodeSpec, } from "@visual-node/core";
4
+ /**
5
+ * Re-registers every previously-installed plugin node (Phase 9 Part B) on startup, from
6
+ * `<projectDir>/.flowserver/plugins/*.node.json` — mirrors `registerBuiltinNodes()`'s own
7
+ * "skip if already registered" idempotency guard (see packages/core/src/nodes/index.ts), so
8
+ * this stays safe to call more than once. A missing `.flowserver/plugins` directory (no
9
+ * plugins ever installed) is treated as "nothing to load", not an error. A single malformed
10
+ * or invalid file is recorded in `failed` and does not abort loading the rest.
11
+ */
12
+ export async function loadInstalledPlugins(projectDir) {
13
+ const pluginsDir = path.join(projectDir, ".flowserver", "plugins");
14
+ let entries;
15
+ try {
16
+ entries = await readdir(pluginsDir);
17
+ }
18
+ catch (err) {
19
+ if (err.code === "ENOENT") {
20
+ return { loaded: [], failed: [] };
21
+ }
22
+ throw err;
23
+ }
24
+ const loaded = [];
25
+ const failed = [];
26
+ for (const file of entries.filter((name) => name.endsWith(".node.json")).sort()) {
27
+ const absolutePath = path.join(pluginsDir, file);
28
+ try {
29
+ const raw = JSON.parse(await readFile(absolutePath, "utf8"));
30
+ const errors = validatePluginNodeSpec(raw);
31
+ if (errors.length > 0) {
32
+ failed.push({ file, error: errors.join("; ") });
33
+ continue;
34
+ }
35
+ const spec = raw;
36
+ if (!getNodeDefinition(spec.type)) {
37
+ registerNode(createPluginNodeDefinition(spec));
38
+ }
39
+ loaded.push(spec.type);
40
+ }
41
+ catch (err) {
42
+ failed.push({ file, error: err instanceof Error ? err.message : String(err) });
43
+ }
44
+ }
45
+ return { loaded, failed };
46
+ }
47
+ //# sourceMappingURL=plugin-loading.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-loading.js","sourceRoot":"","sources":["../src/plugin-loading.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,EACjB,YAAY,EACZ,sBAAsB,GAEvB,MAAM,mBAAmB,CAAC;AAS3B;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,UAAkB;IAC3D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC;IAEnE,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QACpC,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAsC,EAAE,CAAC;IAErD,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAChF,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACjD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;YAC7D,MAAM,MAAM,GAAG,sBAAsB,CAAC,GAAG,CAAC,CAAC;YAC3C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAChD,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,GAAqB,CAAC;YACnC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAClC,YAAY,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAC;YACjD,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Writes README.PLUGIN.md into `projectDir` if it doesn't already exist — a guide to
3
+ * authoring/installing Phase 9 plugin nodes, scaffolded once per project (on every
4
+ * editor-server startup, via `server.ts`) so a user never has to go looking for this
5
+ * documentation elsewhere. Write-once, like the original (pre-merge) package.json
6
+ * behavior: unlike `dependencies`, there's nothing to usefully merge into a prose guide, so
7
+ * an existing file (including one the user has since edited) is never touched again.
8
+ */
9
+ export declare function ensurePluginReadme(projectDir: string): Promise<void>;
@@ -0,0 +1,235 @@
1
+ import { access, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ async function pathExists(target) {
4
+ try {
5
+ await access(target);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ const PLUGIN_README_CONTENT = `# Creating Custom Plugin Nodes for FlowServer
13
+
14
+ FlowServer's node picker isn't limited to the built-in nodes — you can write a single JSON
15
+ file describing a new node (its ports, its config fields, and how it generates code) and
16
+ install it directly from the editor. No TypeScript, no rebuild, no restart.
17
+
18
+ ## Quick start
19
+
20
+ 1. In the editor, click **"Download Plugin Template"** in the toolbar to get a working
21
+ example (an "HTTP Request" node wrapping axios) to copy and modify.
22
+ 2. Edit the file — see the field reference below.
23
+ 3. Click **"Install Plugin"** and pick your file. It's validated immediately (every problem
24
+ is listed at once, not just the first) and, if valid, registered live — it shows up in
25
+ **"Browse Nodes"** right away with an amber "Plugin" badge, no restart needed.
26
+
27
+ Installed plugins are saved into \`.flowserver/plugins/\` inside this project directory and
28
+ are automatically reloaded every time this project is opened — you only need to install
29
+ once per project.
30
+
31
+ ## The plugin file format
32
+
33
+ A plugin is a single JSON object with this shape:
34
+
35
+ \`\`\`json
36
+ {
37
+ "schemaVersion": 1,
38
+ "type": "plugin.myCustomNode",
39
+ "category": "logic",
40
+ "label": "My Custom Node",
41
+ "description": "What this node does, shown in the node picker.",
42
+ "inputs": [
43
+ { "id": "in", "label": "In", "kind": "exec" },
44
+ { "id": "someValue", "label": "Some Value", "kind": "value" }
45
+ ],
46
+ "outputs": [
47
+ { "id": "out", "label": "Out", "kind": "exec" },
48
+ { "id": "result", "label": "Result", "kind": "value" }
49
+ ],
50
+ "configSchema": [
51
+ { "key": "option", "label": "Option", "type": "text", "default": "" }
52
+ ],
53
+ "npmDependencies": { "some-package": "^1.0.0" },
54
+ "async": false,
55
+ "codegen": {
56
+ "imports": ["const somePackage = require(\\"some-package\\");"],
57
+ "body": "const {{result}} = somePackage.doSomething({{someValue}}, {{config.option}});"
58
+ }
59
+ }
60
+ \`\`\`
61
+
62
+ ### Top-level fields
63
+
64
+ | Field | Required | Notes |
65
+ |---|---|---|
66
+ | \`schemaVersion\` | yes | Always \`1\`. |
67
+ | \`type\` | yes | Must match \`plugin.<Name>\` (e.g. \`plugin.httpRequest\`) — the \`plugin.\` prefix is required and keeps you from ever colliding with a built-in node type. Must be globally unique in this project. |
68
+ | \`category\` | yes | One of: \`server\`, \`routing\`, \`middleware\`, \`handler\`, \`logic\`, \`debugging\`, \`operators\`, \`controlFlow\`. Controls the node's header color/icon and which group it appears under in the node picker. Pick whichever existing category best matches what your node does — there is no way to add a new category via a plugin. |
69
+ | \`label\` | yes | Short display name shown on the node and in the picker. |
70
+ | \`description\` | yes | Longer description shown as a tooltip / in the node picker card. |
71
+ | \`inputs\` / \`outputs\` | yes (can be empty arrays) | See "Ports" below. |
72
+ | \`configSchema\` | yes (can be an empty array) | See "Config fields" below. |
73
+ | \`npmDependencies\` | no | \`{ "package-name": "version-range" }\`. Automatically merged into the generated project's \`package.json\` whenever any instance of this node type is used — you never need to declare it separately. |
74
+ | \`async\` | no, default \`false\` | Set to \`true\` if your \`codegen.body\`/\`codegen.setup\` uses \`await\`. This makes the compiler require the containing Route/Function/Middleware's "Async Handler"/"Async Function" checkbox to be enabled, with a clear error if it isn't — instead of silently generating invalid JavaScript. |
75
+ | \`codegen\` | yes | See "Codegen templates" below. |
76
+
77
+ ### Ports
78
+
79
+ Each entry in \`inputs\`/\`outputs\` is \`{ "id": string, "label": string, "kind": "exec" | "value" }\`.
80
+ \`kind\` is **required** on every port (unlike built-in nodes, where it's inferred by
81
+ convention).
82
+
83
+ - **\`"exec"\` ports** connect nodes in a request-handling chain (the white arrowhead pins,
84
+ e.g. Route → your node → another node). Rules:
85
+ - At most **one** exec-input, and its \`id\` **must be \`"in"\`**.
86
+ - At most **one** exec-output, and its \`id\` **must be \`"out"\`**.
87
+ - Omit both entirely to make a pure value-computation node (like a built-in operator —
88
+ no execution position, just a value other nodes can wire from).
89
+ - A plugin **cannot** have more than one exec-output (no Branch/Switch-style forking —
90
+ that's a special codegen capability not available to plugins).
91
+ - **\`"value"\` ports** carry data (the colored-circle pins). You can have any number of
92
+ value **inputs**, but **at most one** value **output**.
93
+ - Value-input pins do **not** get an inline literal editor on the canvas the way built-in
94
+ nodes' pins do — if you want a user-editable default that doesn't require wiring, use a
95
+ \`configSchema\` field instead, and reserve \`inputs\` for things meant to be wired from an
96
+ upstream node.
97
+
98
+ ### Config fields
99
+
100
+ Each entry in \`configSchema\` is
101
+ \`{ "key": string, "label": string, "type": "text"|"select"|"number"|"code"|"boolean", "options"?: string[], "default"?: any, "hint"?: string }\`.
102
+
103
+ - \`"select"\` requires a non-empty \`options\` array.
104
+ - \`"code"\` renders a larger code-editor field and, when referenced in a codegen template
105
+ (see below), is substituted **verbatim** (raw text) rather than quoted — use it for
106
+ anything meant to be actual JavaScript.
107
+ - \`key\` must be unique within \`configSchema\`.
108
+
109
+ ### Codegen templates
110
+
111
+ \`codegen\` is \`{ "imports"?: string[], "setup"?: string, "body"?: string, "order"?: number }\`
112
+ — at least one of \`imports\`/\`setup\`/\`body\` must be present.
113
+
114
+ - **\`imports\`** — one or more \`require(...)\` lines, always safe to place here regardless
115
+ of where the node sits in the flow; they get deduplicated and hoisted to the top of the
116
+ generated file automatically.
117
+ - **\`body\`** — code that runs inside the request handler / function, in this node's
118
+ position in the chain. This is where wired value-inputs and \`req\`/\`res\`-style logic
119
+ belong.
120
+ - **\`setup\`** — code that runs once at the top level of the generated file (outside any
121
+ request handler). Don't reference wired value-inputs here — they're per-request and won't
122
+ exist at module-load time. Most plugins only need \`imports\` + \`body\`.
123
+ - **\`order\`** — only relevant for \`setup\` fragments; controls where among other top-level
124
+ statements this one lands (lower runs earlier). Usually safe to omit.
125
+
126
+ Every string in \`imports\`/\`setup\`/\`body\` is scanned for \`{{placeholder}}\` tokens, each of
127
+ which must be exactly one of:
128
+
129
+ - **\`{{result}}\`** — a stable variable name for this node's own value output. Requires you
130
+ to have declared exactly one \`kind: "value"\` output; conversely, if you declare a value
131
+ output, \`{{result}}\` must appear somewhere in your templates (otherwise the value is
132
+ never assigned, which is almost certainly a mistake and is rejected at install time).
133
+ - **\`{{config.someKey}}\`** — the current value of a \`configSchema\` field named
134
+ \`someKey\` (or its \`default\` if unset). Quoted safely (\`JSON.stringify\`) for every field
135
+ type except \`"code"\`, which is inserted verbatim.
136
+ - **\`{{somePinId}}\`** — a bare id matching a declared \`kind: "value"\` **input** pin.
137
+ Resolves to whatever expression is wired into it, or \`undefined\` if left unwired.
138
+
139
+ All of these are validated the moment you click "Install Plugin" — an undeclared
140
+ placeholder, a placeholder pointing at an exec pin, or a mismatched \`{{result}}\`/value-output
141
+ pairing is rejected with a specific error message before the file is ever saved.
142
+
143
+ ## Two worked examples
144
+
145
+ **Chain-position, async, wraps a network call** (the downloadable template):
146
+
147
+ \`\`\`json
148
+ {
149
+ "schemaVersion": 1,
150
+ "type": "plugin.httpRequest",
151
+ "category": "logic",
152
+ "label": "HTTP Request",
153
+ "description": "Makes an HTTP request via axios and exposes the response.",
154
+ "inputs": [
155
+ { "id": "in", "label": "In", "kind": "exec" },
156
+ { "id": "body", "label": "Body", "kind": "value" }
157
+ ],
158
+ "outputs": [
159
+ { "id": "out", "label": "Out", "kind": "exec" },
160
+ { "id": "response", "label": "Response", "kind": "value" }
161
+ ],
162
+ "configSchema": [
163
+ { "key": "url", "label": "URL", "type": "text", "default": "https://api.example.com" },
164
+ { "key": "method", "label": "Method", "type": "select", "options": ["GET", "POST", "PUT", "DELETE"], "default": "GET" }
165
+ ],
166
+ "npmDependencies": { "axios": "^1.7.0" },
167
+ "async": true,
168
+ "codegen": {
169
+ "imports": ["const axios = require(\\"axios\\");"],
170
+ "body": "const {{result}} = await axios({ method: {{config.method}}, url: {{config.url}}, data: {{body}} });"
171
+ }
172
+ }
173
+ \`\`\`
174
+ Place this in a Route's handler chain and enable that Route's "Async Handler" checkbox.
175
+
176
+ **Terminal, synchronous, zero outputs — a self-contained handler:**
177
+
178
+ \`\`\`json
179
+ {
180
+ "schemaVersion": 1,
181
+ "type": "plugin.uuidResponder",
182
+ "category": "handler",
183
+ "label": "UUID Responder",
184
+ "description": "Generates a UUID via the npm uuid package and responds with it as JSON.",
185
+ "inputs": [{ "id": "in", "label": "In", "kind": "exec" }],
186
+ "outputs": [],
187
+ "configSchema": [],
188
+ "npmDependencies": { "uuid": "^9.0.0" },
189
+ "codegen": {
190
+ "imports": ["const { v4: uuidv4 } = require(\\"uuid\\");"],
191
+ "body": "res.json({ id: uuidv4() });"
192
+ }
193
+ }
194
+ \`\`\`
195
+ No \`async\`, no value output — this node does its own thing and responds directly, exactly
196
+ like the built-in "Send JSON" node's shape (\`outputs: []\`). Wire a Route straight into it
197
+ and nothing needs to come after.
198
+
199
+ These two examples are deliberately as different as possible (different packages,
200
+ different categories, async vs. sync, one value output vs. zero, chain-through vs.
201
+ terminal) to show the range of what a plugin can be — it is **not** limited to
202
+ HTTP-style nodes.
203
+
204
+ ## Limitations to know about
205
+
206
+ - **No update-in-place.** Re-installing a \`type\` that's already registered (built-in or a
207
+ previously installed plugin) is rejected. While iterating on a plugin, just bump the
208
+ \`type\` (e.g. \`plugin.myCustomNode2\`) until you're happy, then clean up the old
209
+ \`.flowserver/plugins/*.node.json\` file if you want.
210
+ - **No multi-arm branching.** A plugin can't create new Branch/Switch-style forks — that
211
+ requires codegen capabilities not exposed to the plugin format. Use the built-in Branch/
212
+ Switch nodes for conditional flow, and plugins for everything else.
213
+ - **At most one value output per plugin.** If you need multiple distinct output values,
214
+ split the work across more than one plugin node, or use a Function node.
215
+ - **Trust model:** a plugin's \`codegen\` strings are spliced verbatim into the generated
216
+ server code — the same trust level as typing raw JavaScript into a Custom Code node.
217
+ There's no sandboxing. This is appropriate for a local, single-developer tool; don't
218
+ install a plugin file from a source you don't trust, same as you wouldn't paste
219
+ untrusted code into a Custom Code node.
220
+ `;
221
+ /**
222
+ * Writes README.PLUGIN.md into `projectDir` if it doesn't already exist — a guide to
223
+ * authoring/installing Phase 9 plugin nodes, scaffolded once per project (on every
224
+ * editor-server startup, via `server.ts`) so a user never has to go looking for this
225
+ * documentation elsewhere. Write-once, like the original (pre-merge) package.json
226
+ * behavior: unlike `dependencies`, there's nothing to usefully merge into a prose guide, so
227
+ * an existing file (including one the user has since edited) is never touched again.
228
+ */
229
+ export async function ensurePluginReadme(projectDir) {
230
+ const readmePath = path.join(projectDir, "README.PLUGIN.md");
231
+ if (await pathExists(readmePath))
232
+ return;
233
+ await writeFile(readmePath, PLUGIN_README_CONTENT, "utf8");
234
+ }
235
+ //# sourceMappingURL=plugin-readme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-readme.js","sourceRoot":"","sources":["../src/plugin-readme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,KAAK,UAAU,UAAU,CAAC,MAAc;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgN7B,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,UAAkB;IACzD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;IAC7D,IAAI,MAAM,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO;IACzC,MAAM,SAAS,CAAC,UAAU,EAAE,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAC7D,CAAC"}
@@ -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))}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.z-50{z-index:50}.\!m-3{margin:.75rem!important}.mx-2\.5{margin-left:.625rem;margin-right:.625rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-2\.5{margin-bottom:.625rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-\[85vh\]{height:85vh}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[80vh\]{max-height:80vh}.max-h-\[90vh\]{max-height:90vh}.w-1{width:.25rem}.w-10{width:2.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-52{width:13rem}.w-72{width:18rem}.w-\[640px\]{width:640px}.w-\[920px\]{width:920px}.w-\[95vw\]{width:95vw}.w-\[min\(1100px\,90vw\)\]{width:min(1100px,90vw)}.w-full{width:100%}.w-screen{width:100vw}.min-w-0{min-width:0px}.min-w-\[190px\]{min-width:190px}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-col-resize{cursor:col-resize}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.resize-y{resize:vertical}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.self-start{align-self:flex-start}.self-center{align-self:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-500\/50{border-color:#f59e0b80}.border-black\/40{border-color:#0006}.border-black\/60{border-color:#0009}.border-green-600{--tw-border-opacity: 1;border-color:rgb(22 163 74 / var(--tw-border-opacity, 1))}.border-neutral-600{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity, 1))}.border-neutral-800{--tw-border-opacity: 1;border-color:rgb(38 38 38 / var(--tw-border-opacity, 1))}.border-red-400{--tw-border-opacity: 1;border-color:rgb(248 113 113 / var(--tw-border-opacity, 1))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-500\/60{border-color:#ef444499}.border-sky-400{--tw-border-opacity: 1;border-color:rgb(56 189 248 / var(--tw-border-opacity, 1))}.bg-\[\#161616\]{--tw-bg-opacity: 1;background-color:rgb(22 22 22 / var(--tw-bg-opacity, 1))}.bg-\[\#1b1b1b\]{--tw-bg-opacity: 1;background-color:rgb(27 27 27 / var(--tw-bg-opacity, 1))}.bg-\[\#1f1f1f\]{--tw-bg-opacity: 1;background-color:rgb(31 31 31 / var(--tw-bg-opacity, 1))}.bg-\[\#242424\]{--tw-bg-opacity: 1;background-color:rgb(36 36 36 / var(--tw-bg-opacity, 1))}.bg-\[\#2a2a2a\]{--tw-bg-opacity: 1;background-color:rgb(42 42 42 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-black\/25{background-color:#00000040}.bg-black\/30{background-color:#0000004d}.bg-black\/40{background-color:#0006}.bg-black\/60{background-color:#0009}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-neutral-600{--tw-bg-opacity: 1;background-color:rgb(82 82 82 / var(--tw-bg-opacity, 1))}.bg-neutral-700{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity, 1))}.bg-neutral-700\/70{background-color:#404040b3}.bg-neutral-800\/40{background-color:#26262666}.bg-neutral-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-red-900\/20{background-color:#7f1d1d33}.bg-red-900\/40{background-color:#7f1d1d66}.bg-sky-600{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.bg-sky-700\/40{background-color:#0369a166}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.from-amber-500{--tw-gradient-from: #f59e0b var(--tw-gradient-from-position);--tw-gradient-to: rgb(245 158 11 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-blue-500{--tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-cyan-500{--tw-gradient-from: #06b6d4 var(--tw-gradient-from-position);--tw-gradient-to: rgb(6 182 212 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-emerald-500{--tw-gradient-from: #10b981 var(--tw-gradient-from-position);--tw-gradient-to: rgb(16 185 129 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-lime-500{--tw-gradient-from: #84cc16 var(--tw-gradient-from-position);--tw-gradient-to: rgb(132 204 22 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-rose-500{--tw-gradient-from: #f43f5e var(--tw-gradient-from-position);--tw-gradient-to: rgb(244 63 94 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-500{--tw-gradient-from: #64748b var(--tw-gradient-from-position);--tw-gradient-to: rgb(100 116 139 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from: #8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to: rgb(139 92 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-amber-800{--tw-gradient-to: #92400e var(--tw-gradient-to-position)}.to-blue-800{--tw-gradient-to: #1e40af var(--tw-gradient-to-position)}.to-cyan-800{--tw-gradient-to: #155e75 var(--tw-gradient-to-position)}.to-emerald-800{--tw-gradient-to: #065f46 var(--tw-gradient-to-position)}.to-lime-800{--tw-gradient-to: #3f6212 var(--tw-gradient-to-position)}.to-rose-800{--tw-gradient-to: #9f1239 var(--tw-gradient-to-position)}.to-slate-700{--tw-gradient-to: #334155 var(--tw-gradient-to-position)}.to-violet-800{--tw-gradient-to: #5b21b6 var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pr-2{padding-right:.5rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.italic{font-style:italic}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.tracking-wide{letter-spacing:.025em}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-neutral-100{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity, 1))}.text-neutral-200{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.text-neutral-300{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-sky-400{--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.text-violet-400{--tw-text-opacity: 1;color:rgb(167 139 250 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-white\/60{color:#fff9}.text-white\/70{color:#ffffffb3}.text-white\/90{color:#ffffffe6}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color: rgb(0 0 0 / .4);--tw-shadow: var(--tw-shadow-colored)}.shadow-black\/50{--tw-shadow-color: rgb(0 0 0 / .5);--tw-shadow: var(--tw-shadow-colored)}.shadow-black\/60{--tw-shadow-color: rgb(0 0 0 / .6);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-red-500{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1))}.ring-sky-400{--tw-ring-opacity: 1;--tw-ring-color: rgb(56 189 248 / var(--tw-ring-opacity, 1))}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}html,body,#root{background-color:#1b1b1b;height:100%}.placeholder\:text-neutral-600::-moz-placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.placeholder\:text-neutral-600::placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.last\:mb-0:last-child{margin-bottom:0}.hover\:border-red-500:hover{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.hover\:border-sky-500:hover{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-green-600\/10:hover{background-color:#16a34a1a}.hover\:bg-neutral-700:hover{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity, 1))}.hover\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.hover\:bg-red-500\/10:hover{background-color:#ef44441a}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-sky-500\/10:hover{background-color:#0ea5e91a}.hover\:bg-sky-500\/70:hover{background-color:#0ea5e9b3}.hover\:bg-sky-700:hover{--tw-bg-opacity: 1;background-color:rgb(3 105 161 / var(--tw-bg-opacity, 1))}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:text-neutral-100:hover{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity, 1))}.hover\:text-neutral-200:hover{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.hover\:text-neutral-300:hover{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-sky-300:hover{--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity, 1))}.hover\:text-sky-400:hover{--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.focus\:border-sky-500:focus{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.active\:bg-sky-500\/70:active{background-color:#0ea5e9b3}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}