diff --git a/.github/workflows/ci_linux.yaml b/.github/workflows/ci_linux.yaml index ba3a0358..56f38eed 100644 --- a/.github/workflows/ci_linux.yaml +++ b/.github/workflows/ci_linux.yaml @@ -31,6 +31,14 @@ jobs: - name: Setup rust run: rustup default ${{ matrix.rust-version }} + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + + - name: Setup nodejs + run: pnpm env use --global lts + - name: Build default features run: cargo build --workspace - name: Test default features diff --git a/.gitignore b/.gitignore index 543a865d..566d5abb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,14 @@ debug/ target/ Cargo.lock + +# nodejs +node_modules + +# diagram-editor +/diagram-editor/dist +/diagram-editor/dist.tar.gz + +# storybook +*storybook.log +storybook-static diff --git a/Cargo.toml b/Cargo.toml index 884bbd4e..0e4fbcd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,8 +78,10 @@ test-log = { version = "0.2.16", features = [ [workspace] members = [ + "examples/diagram/calculator", "examples/zenoh-examples", + "diagram-editor", ] [[bin]] diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..88522160 --- /dev/null +++ b/biome.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.0.5/schema.json", + "files": { + "includes": [ + "**/*.tsx", + "**/*.ts", + "**/*.mjs", + "**/*.cjs", + "**/*.json", + "**/*.css", + "!**/*.schema.json", + "!node_modules/**" + ] + }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "indentStyle": "space" + }, + "javascript": { + "formatter": { + "quoteStyle": "single" + } + }, + "css": { + "parser": { + "cssModules": true + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "useExhaustiveDependencies": { + "level": "error", + "options": { + "hooks": [ + { "name": "useEditorMode", "stableResult": [1] }, + { "name": "useTemplates", "stableResult": [1] } + ] + } + } + } + } + }, + "overrides": [ + { + "includes": ["**/*.test.ts", "**/*.test.tsx"], + "linter": { + "rules": { + "style": { + "noNonNullAssertion": "off" + } + } + } + } + ] +} diff --git a/diagram-editor/.gitignore b/diagram-editor/.gitignore new file mode 100644 index 00000000..d6f1f613 --- /dev/null +++ b/diagram-editor/.gitignore @@ -0,0 +1,4 @@ +/dist + +*storybook.log +storybook-static diff --git a/diagram-editor/.storybook/main.ts b/diagram-editor/.storybook/main.ts new file mode 100644 index 00000000..06f27661 --- /dev/null +++ b/diagram-editor/.storybook/main.ts @@ -0,0 +1,11 @@ +import type { StorybookConfig } from 'storybook-react-rsbuild'; + +const config: StorybookConfig = { + stories: [ + '../frontend/**/*.mdx', + '../frontend/**/*.stories.@(js|jsx|mjs|ts|tsx)', + ], + addons: [], + framework: 'storybook-react-rsbuild', +}; +export default config; diff --git a/diagram-editor/.storybook/preview.ts b/diagram-editor/.storybook/preview.ts new file mode 100644 index 00000000..806c2128 --- /dev/null +++ b/diagram-editor/.storybook/preview.ts @@ -0,0 +1,14 @@ +import type { Preview } from 'storybook-react-rsbuild'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/diagram-editor/Cargo.toml b/diagram-editor/Cargo.toml new file mode 100644 index 00000000..ae57b5cd --- /dev/null +++ b/diagram-editor/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "bevy_impulse_diagram_editor" +version = "0.0.1" +edition = "2021" +authors = ["Teo Koon Peng "] +license = "Apache-2.0" +description = "Frontend for bevy_impulse diagrams" +readme = "README.md" +repository = "https://github.com/open-rmf/bevy_impulse" +keywords = [ + "reactive", + "workflow", + "behavior", + "agent", + "bevy", + "frontend", + "diagram", +] +categories = [ + "science::robotics", + "asynchronous", + "concurrency", + "game-development", +] +exclude = ["/build.rs"] + +[lib] +path = "server/lib.rs" + +[dependencies] +axum = { version = "0.8.4", features = ["ws"] } +bevy_app = "0.12.1" +bevy_ecs = "0.12.1" +bevy_impulse = { version = "0.0.2", path = "..", features = [ + "diagram", + "trace", +] } +flate2 = { version = "1.1.1", optional = true } +futures-util = "0.3.31" +indexmap = { version = "2.10.0", optional = true, features = ["serde"] } +mime_guess = "2.0.5" +schemars = { version = "0.9", optional = true } +serde = "1.0.219" +serde_json = "1.0.140" +tar = { version = "0.4.44", optional = true } +tokio = "1.45.1" +tracing = "0.1.41" + +[build-dependencies] +flate2 = { version = "1.1.1", optional = true } +tar = { version = "0.4.44", optional = true } + +[dev-dependencies] +futures-channel = "0.3.31" +test-log = "0.2.18" +tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] } +tower = "0.5.2" + +[features] +default = ["frontend"] +frontend = ["dep:flate2", "dep:tar"] +json_schema = ["dep:schemars", "dep:indexmap"] + +[[bin]] +name = "print_schema" +path = "server/bin/print_schema.rs" +required-features = ["json_schema"] diff --git a/diagram-editor/README.md b/diagram-editor/README.md new file mode 100644 index 00000000..d46da96f --- /dev/null +++ b/diagram-editor/README.md @@ -0,0 +1,77 @@ +# bevy_impulse_diagram_editor + +![](./docs/assets/diagram-editor-preview.webp) + +This contains a SPA React web app to create and edit a `bevy_impulse` diagram and an axum router to serve it. + +## Setup + +Install pnpm and nodejs: + +```bash +curl -fsSL https://get.pnpm.io/install.sh | bash - +pnpm env use --global lts +``` + +Install the dependencies: + +```bash +pnpm install +``` + +## Embedding the Diagram Editor into a `bevy_impulse` app + +The frontend is built using `rsbuild` and embedded inside the crate. The library exposes an axum router that can be used to serve both the frontend and backend: + +```rs +use bevy_impulse_diagram_editor::{new_router, ServerOptions}; + +fn main() { + let mut registry = DiagramElementRegistry::new(); + // register node builders, section builders etc. + + let mut app = bevy_app::App::new(); + app.add_plugins(ImpulseAppPlugin::default()); + let router = new_router(&mut app, registry, ServerOptions::default()); + let listener = tokio::net::TcpListener::bind(("localhost", 3000)) + .await + .unwrap(); + axum::serve(listener, router).await?; +} +``` + +To omit the frontend and serve only the backend API, disable the default features: + +```toml +[dependencies] +bevy_impulse_diagram_editor = { version = "0.0.1", default-features = false } +``` + +See the [calculator demo](../examples/diagram/calculator) for more examples. + +## Local development server + +Normally the web stack is not required by using this crate as a dependency, but it is required when developing the frontend. + +Requirements: + +* nodejs +* pnpm + +First start the `dev` backend server: + +```bash +pnpm dev:backend +``` + +then in another terminal, start the frontend `dev` server: + +```bash +pnpm dev +``` + +When there are breaking changes in `bevy_impulse`, the typescript definitions need to be regenerated: + +```bash +pnpm generate-types +``` diff --git a/diagram-editor/build.rs b/diagram-editor/build.rs new file mode 100644 index 00000000..670e36d3 --- /dev/null +++ b/diagram-editor/build.rs @@ -0,0 +1,75 @@ +/// Builds the frontend and packages it into a tar.gz archive. +/// This requires `pnpm` and all the js dependencies to be available. +/// +/// This build script is excluded from the crate, the output tarball is included instead. +/// This allows downstream to build the crate without any of the js stack. + +#[cfg(feature = "frontend")] +mod frontend { + use flate2::{write::GzEncoder, Compression}; + use std::fs::File; + use std::path::PathBuf; + use std::process::Command; + use tar::Builder; + + pub fn build_frontend() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=package.json"); + println!("cargo:rerun-if-changed=../pnpm-lock.yaml"); + println!("cargo:rerun-if-changed=rsbuild.config.ts"); + println!("cargo:rerun-if-changed=frontend"); + + let status = Command::new("pnpm") + .arg("install") + .arg("--frozen-lockfile") + .status() + .expect("Failed to execute pnpm install"); + + if !status.success() { + panic!("pnpm install failed with status: {:?}", status); + } + + let status = Command::new("pnpm") + .arg("build") + .status() + .expect("Failed to execute pnpm build"); + + if !status.success() { + panic!("pnpm build failed with status: {:?}", status); + } + + let dist_dir_path = "dist"; + // We put the output in `CARGO_MANIFEST_DIR` instead of `OUT_DIR` because we want to include + // it in the crate. + let out_dir = + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set")); + let output_tar_gz_path = out_dir.join("dist.tar.gz"); + + if std::path::Path::new(dist_dir_path).exists() { + let tar_gz_file = File::create(&output_tar_gz_path) + .expect("Failed to create output tar.gz file in CARGO_MANIFEST_DIR"); + let enc = GzEncoder::new(tar_gz_file, Compression::default()); + let mut tar_builder = Builder::new(enc); + + // Add the entire "dist" directory to the archive, preserving its name within the archive. + tar_builder + .append_dir_all(".", dist_dir_path) + .expect("Failed to add directory to tar archive"); + tar_builder.finish().expect("Failed to finish tar archive"); + println!( + "Successfully compressed '{}' into '{:?}'", + dist_dir_path, output_tar_gz_path + ); + } else { + panic!( + "Directory '{}' not found after pnpm build. Frontend build might have failed.", + dist_dir_path + ); + } + } +} + +fn main() { + #[cfg(feature = "frontend")] + frontend::build_frontend(); +} diff --git a/diagram-editor/docs/assets/diagram-editor-preview.webp b/diagram-editor/docs/assets/diagram-editor-preview.webp new file mode 100644 index 00000000..988a56fc Binary files /dev/null and b/diagram-editor/docs/assets/diagram-editor-preview.webp differ diff --git a/diagram-editor/frontend/add-operation.tsx b/diagram-editor/frontend/add-operation.tsx new file mode 100644 index 00000000..1bd30977 --- /dev/null +++ b/diagram-editor/frontend/add-operation.tsx @@ -0,0 +1,390 @@ +import { Button, ButtonGroup, styled } from '@mui/material'; +import type { NodeAddChange, XYPosition } from '@xyflow/react'; +import React from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { EditorMode, useEditorMode } from './editor-mode'; +import { useNodeManager } from './node-manager'; +import type { DiagramEditorNode } from './nodes'; +import { + BufferAccessIcon, + BufferIcon, + createOperationNode, + createScopeNode, + createSectionBufferNode, + createSectionInputNode, + createSectionOutputNode, + ForkCloneIcon, + ForkResultIcon, + isOperationNode, + isSectionBufferNode, + isSectionInputNode, + isSectionOutputNode, + JoinIcon, + ListenIcon, + NodeIcon, + ScopeIcon, + SectionBufferIcon, + type SectionBufferNode, + SectionIcon, + SectionInputIcon, + type SectionInputNode, + SectionOutputIcon, + type SectionOutputNode, + SerializedJoinIcon, + SplitIcon, + StreamOutIcon, + TransformIcon, + UnzipIcon, +} from './nodes'; +import type { DiagramOperation, NextOperation } from './types/api'; +import { joinNamespaces, ROOT_NAMESPACE } from './utils/namespace'; +import { addUniqueSuffix } from './utils/unique-value'; + +const StyledOperationButton = styled(Button)({ + justifyContent: 'flex-start', +}); + +export interface AddOperationProps { + parentId?: string; + newNodePosition: XYPosition; + onAdd?: (change: NodeAddChange[]) => void; +} + +function createSectionInputChange( + remappedId: string, + targetId: NextOperation, + position: XYPosition, +): NodeAddChange { + return { + type: 'add', + item: createSectionInputNode(remappedId, targetId, position), + }; +} + +function createSectionOutputChange( + outputId: string, + position: XYPosition, +): NodeAddChange { + return { + type: 'add', + item: createSectionOutputNode(outputId, position), + }; +} + +function createSectionBufferChange( + remappedId: string, + targetId: NextOperation, + position: XYPosition, +): NodeAddChange { + return { + type: 'add', + item: createSectionBufferNode(remappedId, targetId, position), + }; +} + +function createNodeChange( + namespace: string, + parentId: string | undefined, + newNodePosition: XYPosition, + op: DiagramOperation, +): NodeAddChange[] { + if (op.type === 'scope') { + return createScopeNode( + namespace, + parentId, + newNodePosition, + op, + uuidv4(), + ).map((node) => ({ type: 'add', item: node })); + } + + return [ + { + type: 'add', + item: createOperationNode( + namespace, + parentId, + newNodePosition, + op, + uuidv4(), + ), + }, + ]; +} + +function AddOperation({ parentId, newNodePosition, onAdd }: AddOperationProps) { + const [editorMode] = useEditorMode(); + const nodeManager = useNodeManager(); + const namespace = React.useMemo(() => { + const parentNode = parentId && nodeManager.tryGetNode(parentId); + if (!parentNode || !isOperationNode(parentNode)) { + return ROOT_NAMESPACE; + } + return joinNamespaces(parentNode.data.namespace, parentNode.data.opId); + }, [parentId, nodeManager]); + + return ( + + {editorMode.mode === EditorMode.Template && + namespace === ROOT_NAMESPACE && ( + } + onClick={() => { + const remappedId = addUniqueSuffix( + 'new_input', + nodeManager.nodes + .filter(isSectionInputNode) + .map((n) => n.data.remappedId), + ); + onAdd?.([ + createSectionInputChange( + remappedId, + { builtin: 'dispose' }, + newNodePosition, + ), + ]); + }} + > + Section Input + + )} + {editorMode.mode === EditorMode.Template && + namespace === ROOT_NAMESPACE && ( + } + onClick={() => { + const outputId = addUniqueSuffix( + 'new_output', + nodeManager.nodes + .filter(isSectionOutputNode) + .map((n) => n.data.outputId), + ); + onAdd?.([createSectionOutputChange(outputId, newNodePosition)]); + }} + > + Section Output + + )} + {editorMode.mode === EditorMode.Template && + namespace === ROOT_NAMESPACE && ( + } + onClick={() => { + const remappedId = addUniqueSuffix( + 'new_buffer', + nodeManager.nodes + .filter(isSectionBufferNode) + .map((n) => n.data.remappedId), + ); + onAdd?.([ + createSectionBufferChange( + remappedId, + { builtin: 'dispose' }, + newNodePosition, + ), + ]); + }} + > + Section Buffer + + )} + } + onClick={() => { + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'node', + builder: '', + next: { builtin: 'dispose' }, + }), + ); + }} + > + Node + + {/* */} + } + onClick={() => + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'fork_clone', + next: [], + }), + ) + } + > + Fork Clone + + } + onClick={() => + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'unzip', + next: [], + }), + ) + } + > + Unzip + + } + onClick={() => { + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'fork_result', + err: { builtin: 'dispose' }, + ok: { builtin: 'dispose' }, + }), + ); + }} + > + Fork Result + + } + onClick={() => + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'split', + }), + ) + } + > + Split + + } + onClick={() => { + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'join', + buffers: [], + next: { builtin: 'dispose' }, + }), + ); + }} + > + Join + + } + onClick={() => + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'serialized_join', + buffers: [], + next: { builtin: 'dispose' }, + }), + ) + } + > + Serialized Join + + } + onClick={() => { + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'transform', + cel: '', + next: { builtin: 'dispose' }, + }), + ); + }} + > + Transform + + } + onClick={() => + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'buffer', + }), + ) + } + > + Buffer + + } + onClick={() => { + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'buffer_access', + buffers: [], + next: { builtin: 'dispose' }, + }), + ); + }} + > + Buffer Access + + } + onClick={() => + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'listen', + buffers: [], + next: { builtin: 'dispose' }, + }), + ) + } + > + Listen + + } + onClick={() => + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'stream_out', + name: '', + }), + ) + } + > + Stream Out + + } + onClick={() => + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'scope', + start: { builtin: 'dispose' }, + ops: {}, + next: { builtin: 'dispose' }, + }), + ) + } + > + Scope + + } + onClick={() => + onAdd?.( + createNodeChange(namespace, parentId, newNodePosition, { + type: 'section', + template: '', + }), + ) + } + > + Section + + + ); +} + +export default AddOperation; diff --git a/diagram-editor/frontend/api-client-provider.tsx b/diagram-editor/frontend/api-client-provider.tsx new file mode 100644 index 00000000..9d31dc5f --- /dev/null +++ b/diagram-editor/frontend/api-client-provider.tsx @@ -0,0 +1,8 @@ +import { createContext, useContext } from 'react'; +import { ApiClient } from './api-client'; + +const ApiClientContext = createContext(new ApiClient()); + +export const useApiClient = () => { + return useContext(ApiClientContext); +}; diff --git a/diagram-editor/frontend/api-client/api-client.ts b/diagram-editor/frontend/api-client/api-client.ts new file mode 100644 index 00000000..efec7e96 --- /dev/null +++ b/diagram-editor/frontend/api-client/api-client.ts @@ -0,0 +1,76 @@ +import { from, type Observable } from 'rxjs'; +import type { + Diagram, + DiagramElementRegistry, + PostRunRequest, +} from '../types/api'; +import { getSchema } from '../utils/ajv'; +import { DebugSession } from './debug-session'; + +const validateRegistry = getSchema( + 'DiagramElementRegistry', +); + +async function getErrorMessage(response: Response) { + const text = await response.text(); + return text || `${response.status} ${response.statusText}`; +} + +export class ApiClient { + getRegistry(): Observable { + return from( + (async () => { + const response = await fetch('/api/registry'); + if (!response.ok) { + throw new Error(await getErrorMessage(response)); + } + const data = await response.json(); + if (!validateRegistry(data)) { + throw validateRegistry.errors; + } + return data; + })(), + ); + } + + postRunWorkflow(diagram: Diagram, request: unknown): Observable { + return from( + (async () => { + const body: PostRunRequest = { + diagram, + request, + }; + const response = await fetch('/api/executor/run', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + if (!response.ok) { + throw new Error(await getErrorMessage(response)); + } + return response.json(); + })(), + ); + } + + async wsDebugWorkflow( + diagram: Diagram, + request: unknown, + ): Promise { + const ws = new WebSocket('/api/executor/debug'); + await new Promise((resolve, reject) => { + ws.onopen = () => { + const body: PostRunRequest = { + diagram, + request, + }; + ws.send(JSON.stringify(body)); + resolve(ws); + }; + ws.onerror = reject; + }); + return new DebugSession(ws); + } +} diff --git a/diagram-editor/frontend/api-client/debug-session.ts b/diagram-editor/frontend/api-client/debug-session.ts new file mode 100644 index 00000000..102262c5 --- /dev/null +++ b/diagram-editor/frontend/api-client/debug-session.ts @@ -0,0 +1,28 @@ +import { type Observable, Subject } from 'rxjs'; +import type { DebugSessionMessage } from '../types/api'; +import { getSchema } from '../utils/ajv'; + +const validateDebugSessionMessage = getSchema( + 'DebugSessionMessage', +); + +export class DebugSession { + debugFeedback$: Observable; + + constructor(ws: WebSocket) { + const debugFeedbackSubject$ = new Subject(); + ws.onmessage = (ev) => { + try { + const msg = JSON.parse(ev.data); + if (!validateDebugSessionMessage(msg)) { + console.error(validateDebugSessionMessage.errors); + return; + } + debugFeedbackSubject$.next(msg); + } catch (e) { + console.error((e as Error).message); + } + }; + this.debugFeedback$ = debugFeedbackSubject$; + } +} diff --git a/diagram-editor/frontend/api-client/index.ts b/diagram-editor/frontend/api-client/index.ts new file mode 100644 index 00000000..2404e084 --- /dev/null +++ b/diagram-editor/frontend/api-client/index.ts @@ -0,0 +1,2 @@ +export * from './api-client'; +export * from './debug-session'; diff --git a/diagram-editor/frontend/api.preprocessed.schema.json b/diagram-editor/frontend/api.preprocessed.schema.json new file mode 100644 index 00000000..fbe2a406 --- /dev/null +++ b/diagram-editor/frontend/api.preprocessed.schema.json @@ -0,0 +1,1297 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BufferAccessSchema": { + "description": "Zip a message together with access to one or more buffers.\n\n The receiving node must have an input type of `(Message, Keys)`\n where `Keys` implements the [`Accessor`][1] trait.\n\n [1]: crate::Accessor\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"fork_clone\",\n \"ops\": {\n \"fork_clone\": {\n \"type\": \"fork_clone\",\n \"next\": [\"num_output\", \"string_output\"]\n },\n \"num_output\": {\n \"type\": \"node\",\n \"builder\": \"num_output\",\n \"next\": \"buffer_access\"\n },\n \"string_output\": {\n \"type\": \"node\",\n \"builder\": \"string_output\",\n \"next\": \"string_buffer\"\n },\n \"string_buffer\": {\n \"type\": \"buffer\"\n },\n \"buffer_access\": {\n \"type\": \"buffer_access\",\n \"buffers\": [\"string_buffer\"],\n \"target_node\": \"with_buffer_access\",\n \"next\": \"with_buffer_access\"\n },\n \"with_buffer_access\": {\n \"type\": \"node\",\n \"builder\": \"with_buffer_access\",\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```", + "properties": { + "buffers": { + "$ref": "#/$defs/BufferSelection" + }, + "display_text": { + "description": "Override for text that should be displayed for an operation within an\n editor.", + "type": [ + "string", + "null" + ] + }, + "next": { + "$ref": "#/$defs/NextOperation" + }, + "trace": { + "anyOf": [ + { + "$ref": "#/$defs/TraceToggle" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "next", + "buffers" + ], + "type": "object" + }, + "BufferSchema": { + "description": "Create a [`Buffer`][1] which can be used to store and pull data within\n a scope.\n\n By default the [`BufferSettings`][2] will keep the single last message\n pushed to the buffer. You can change that with the optional `settings`\n property.\n\n Use the `\"serialize\": true` option to serialize the messages into\n [`JsonMessage`] before they are inserted into the buffer. This\n allows any serializable message type to be pushed into the buffer. If\n left unspecified, the buffer will store the specific data type that gets\n pushed into it. If the buffer inputs are not being serialized, then all\n incoming messages being pushed into the buffer must have the same type.\n\n [1]: crate::Buffer\n [2]: crate::BufferSettings\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"fork_clone\",\n \"ops\": {\n \"fork_clone\": {\n \"type\": \"fork_clone\",\n \"next\": [\"num_output\", \"string_output\", \"all_num_buffer\", \"serialized_num_buffer\"]\n },\n \"num_output\": {\n \"type\": \"node\",\n \"builder\": \"num_output\",\n \"next\": \"buffer_access\"\n },\n \"string_output\": {\n \"type\": \"node\",\n \"builder\": \"string_output\",\n \"next\": \"string_buffer\"\n },\n \"string_buffer\": {\n \"type\": \"buffer\",\n \"settings\": {\n \"retention\": { \"keep_last\": 10 }\n }\n },\n \"all_num_buffer\": {\n \"type\": \"buffer\",\n \"settings\": {\n \"retention\": \"keep_all\"\n }\n },\n \"serialized_num_buffer\": {\n \"type\": \"buffer\",\n \"serialize\": true\n },\n \"buffer_access\": {\n \"type\": \"buffer_access\",\n \"buffers\": [\"string_buffer\"],\n \"target_node\": \"with_buffer_access\",\n \"next\": \"with_buffer_access\"\n },\n \"with_buffer_access\": {\n \"type\": \"node\",\n \"builder\": \"with_buffer_access\",\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```", + "properties": { + "display_text": { + "description": "Override for text that should be displayed for an operation within an\n editor.", + "type": [ + "string", + "null" + ] + }, + "serialize": { + "description": "If true, messages will be serialized before sending into the buffer.", + "type": [ + "boolean", + "null" + ] + }, + "settings": { + "$ref": "#/$defs/BufferSettings" + }, + "trace": { + "anyOf": [ + { + "$ref": "#/$defs/TraceToggle" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "BufferSelection": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/NextOperation" + }, + "type": "object" + }, + { + "items": { + "$ref": "#/$defs/NextOperation" + }, + "type": "array" + } + ] + }, + "BufferSettings": { + "description": "Settings to describe the behavior of a buffer.", + "properties": { + "retention": { + "$ref": "#/$defs/RetentionPolicy" + } + }, + "required": [ + "retention" + ], + "type": "object" + }, + "BuiltinTarget": { + "oneOf": [ + { + "const": "terminate", + "description": "Use the output to terminate the current scope. The value passed into\n this operation will be the return value of the scope.", + "type": "string" + }, + { + "const": "dispose", + "description": "Dispose of the output.", + "type": "string" + }, + { + "const": "cancel", + "description": "When triggered, cancel the current scope. If this is an inner scope of a\n workflow then the parent scope will see a disposal happen. If this is\n the root scope of a workflow then the whole workflow will cancel.", + "type": "string" + } + ] + }, + "DebugSessionMessage": { + "oneOf": [ + { + "allOf": [ + { + "oneOf": [ + { + "properties": { + "operationStarted": { + "type": "string" + } + }, + "required": [ + "operationStarted" + ], + "type": "object" + } + ] + }, + { + "properties": { + "type": { + "const": "feedback", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "oneOf": [ + { + "properties": { + "ok": true + }, + "required": [ + "ok" + ], + "type": "object" + }, + { + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ], + "type": "object" + } + ] + }, + { + "properties": { + "type": { + "const": "finish", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + } + ] + }, + "Diagram": { + "properties": { + "default_trace": { + "$ref": "#/$defs/TraceToggle" + }, + "on_implicit_error": { + "anyOf": [ + { + "$ref": "#/$defs/NextOperation" + }, + { + "type": "null" + } + ] + }, + "ops": { + "additionalProperties": { + "$ref": "#/$defs/DiagramOperation" + }, + "description": "Operations that define the workflow", + "type": "object" + }, + "start": { + "$ref": "#/$defs/NextOperation" + }, + "templates": { + "additionalProperties": { + "$ref": "#/$defs/SectionTemplate" + }, + "default": {}, + "type": "object" + }, + "version": { + "description": "Version of the diagram, should always be `0.1.0`.", + "type": "string" + } + }, + "required": [ + "version", + "start", + "ops" + ], + "type": "object" + }, + "DiagramElementRegistry": { + "properties": { + "messages": { + "additionalProperties": { + "$ref": "#/$defs/MessageRegistration" + }, + "type": "object" + }, + "nodes": { + "additionalProperties": { + "$ref": "#/$defs/NodeRegistration" + }, + "type": "object" + }, + "schemas": { + "additionalProperties": true, + "type": "object" + }, + "sections": { + "additionalProperties": { + "$ref": "#/$defs/SectionRegistration" + }, + "type": "object" + }, + "trace_supported": { + "type": "boolean" + } + }, + "required": [ + "nodes", + "sections", + "trace_supported", + "messages", + "schemas" + ], + "type": "object" + }, + "DiagramOperation": { + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/NodeSchema" + }, + { + "properties": { + "type": { + "const": "node", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/SectionSchema" + }, + { + "properties": { + "type": { + "const": "section", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/ScopeSchema" + }, + { + "properties": { + "type": { + "const": "scope", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/StreamOutSchema" + }, + { + "properties": { + "type": { + "const": "stream_out", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/ForkCloneSchema" + }, + { + "properties": { + "type": { + "const": "fork_clone", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/UnzipSchema" + }, + { + "properties": { + "type": { + "const": "unzip", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/ForkResultSchema" + }, + { + "properties": { + "type": { + "const": "fork_result", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/SplitSchema" + }, + { + "properties": { + "type": { + "const": "split", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/JoinSchema" + }, + { + "properties": { + "type": { + "const": "join", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/SerializedJoinSchema" + }, + { + "properties": { + "type": { + "const": "serialized_join", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/TransformSchema" + }, + { + "properties": { + "type": { + "const": "transform", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/BufferSchema" + }, + { + "properties": { + "type": { + "const": "buffer", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/BufferAccessSchema" + }, + { + "properties": { + "type": { + "const": "buffer_access", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/ListenSchema" + }, + { + "properties": { + "type": { + "const": "listen", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + } + ] + }, + "ForkCloneSchema": { + "description": "If the request is cloneable, clone it into multiple responses that can\n each be sent to a different operation. The `next` property is an array.\n\n This creates multiple simultaneous branches of execution within the\n workflow. Usually when you have multiple branches you will either\n * race - connect all branches to `terminate` and the first branch to\n finish \"wins\" the race and gets to the be output\n * join - connect each branch into a buffer and then use the `join`\n operation to reunite them\n * collect - TODO(@mxgrey): [add the collect operation](https://github.com/open-rmf/bevy_impulse/issues/59)\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"begin_race\",\n \"ops\": {\n \"begin_race\": {\n \"type\": \"fork_clone\",\n \"next\": [\n \"ferrari\",\n \"mustang\"\n ]\n },\n \"ferrari\": {\n \"type\": \"node\",\n \"builder\": \"drive\",\n \"config\": \"ferrari\",\n \"next\": { \"builtin\": \"terminate\" }\n },\n \"mustang\": {\n \"type\": \"node\",\n \"builder\": \"drive\",\n \"config\": \"mustang\",\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())", + "properties": { + "display_text": { + "description": "Override for text that should be displayed for an operation within an\n editor.", + "type": [ + "string", + "null" + ] + }, + "next": { + "items": { + "$ref": "#/$defs/NextOperation" + }, + "type": "array" + }, + "trace": { + "anyOf": [ + { + "$ref": "#/$defs/TraceToggle" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "next" + ], + "type": "object" + }, + "ForkResultSchema": { + "description": "If the request is a [`Result`], send the output message down an\n `ok` branch or down an `err` branch depending on whether the result has\n an [`Ok`] or [`Err`] value. The `ok` branch will receive a `T` while the\n `err` branch will receive an `E`.\n\n Only one branch will be activated by each input message that enters the\n operation.\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"fork_result\",\n \"ops\": {\n \"fork_result\": {\n \"type\": \"fork_result\",\n \"ok\": { \"builtin\": \"terminate\" },\n \"err\": { \"builtin\": \"dispose\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```", + "properties": { + "display_text": { + "description": "Override for text that should be displayed for an operation within an\n editor.", + "type": [ + "string", + "null" + ] + }, + "err": { + "$ref": "#/$defs/NextOperation" + }, + "ok": { + "$ref": "#/$defs/NextOperation" + }, + "trace": { + "anyOf": [ + { + "$ref": "#/$defs/TraceToggle" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "ok", + "err" + ], + "type": "object" + }, + "InputRemapping": { + "anyOf": [ + { + "description": "Do a simple 1:1 forwarding of the names listed in the array", + "items": { + "type": "string" + }, + "type": "array" + }, + { + "additionalProperties": { + "$ref": "#/$defs/NextOperation" + }, + "description": "Rename an operation inside the section to expose it externally. The key\n of the map is what siblings of the section can connect to, and the value\n of the entry is the identifier of the input inside the section that is\n being exposed.\n\n This allows a section to expose inputs and buffers that are provided\n by inner sections.", + "type": "object" + } + ] + }, + "JoinSchema": { + "description": "Wait for exactly one item to be available in each buffer listed in\n `buffers`, then join each of those items into a single output message\n that gets sent to `next`.\n\n If the `next` operation is not a `node` type (e.g. `fork_clone`) then\n you must specify a `target_node` so that the diagram knows what data\n structure to join the values into.\n\n The output message type must be registered as joinable at compile time.\n If you want to join into a dynamic data structure then you should use\n [`DiagramOperation::SerializedJoin`] instead.\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"begin_measuring\",\n \"ops\": {\n \"begin_measuring\": {\n \"type\": \"fork_clone\",\n \"next\": [\"localize\", \"imu\"]\n },\n \"localize\": {\n \"type\": \"node\",\n \"builder\": \"localize\",\n \"next\": \"estimated_position\"\n },\n \"imu\": {\n \"type\": \"node\",\n \"builder\": \"imu\",\n \"config\": \"velocity\",\n \"next\": \"estimated_velocity\"\n },\n \"estimated_position\": { \"type\": \"buffer\" },\n \"estimated_velocity\": { \"type\": \"buffer\" },\n \"gather_state\": {\n \"type\": \"join\",\n \"buffers\": {\n \"position\": \"estimate_position\",\n \"velocity\": \"estimate_velocity\"\n },\n \"next\": \"report_state\"\n },\n \"report_state\": {\n \"type\": \"node\",\n \"builder\": \"publish_state\",\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```", + "properties": { + "buffers": { + "$ref": "#/$defs/BufferSelection" + }, + "next": { + "$ref": "#/$defs/NextOperation" + } + }, + "required": [ + "next", + "buffers" + ], + "type": "object" + }, + "ListenSchema": { + "description": "Listen on a buffer.\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"num_output\",\n \"ops\": {\n \"buffer\": {\n \"type\": \"buffer\"\n },\n \"num_output\": {\n \"type\": \"node\",\n \"builder\": \"num_output\",\n \"next\": \"buffer\"\n },\n \"listen\": {\n \"type\": \"listen\",\n \"buffers\": [\"buffer\"],\n \"target_node\": \"listen_buffer\",\n \"next\": \"listen_buffer\"\n },\n \"listen_buffer\": {\n \"type\": \"node\",\n \"builder\": \"listen_buffer\",\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())", + "properties": { + "buffers": { + "$ref": "#/$defs/BufferSelection" + }, + "next": { + "$ref": "#/$defs/NextOperation" + }, + "target_node": { + "anyOf": [ + { + "$ref": "#/$defs/NextOperation" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "next", + "buffers" + ], + "type": "object" + }, + "MessageOperation": { + "properties": { + "deserialize": { + "type": [ + "object", + "null" + ] + }, + "fork_clone": { + "type": [ + "object", + "null" + ] + }, + "fork_result": { + "type": [ + "object", + "null" + ] + }, + "join": { + "type": [ + "object", + "null" + ] + }, + "serialize": { + "type": [ + "object", + "null" + ] + }, + "split": { + "type": [ + "object", + "null" + ] + }, + "unzip": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "MessageRegistration": { + "properties": { + "operations": { + "$ref": "#/$defs/MessageOperation" + }, + "schema": { + "anyOf": [ + { + "$ref": "#/$defs/Schema" + }, + { + "type": "null" + } + ] + }, + "type_name": { + "type": "string" + } + }, + "required": [ + "type_name", + "operations" + ], + "type": "object" + }, + "NamespacedOperation": { + "additionalProperties": { + "type": "string" + }, + "description": "Refer to an operation inside of a namespace, e.g. { \"\": \"\"", + "maxProperties": 1, + "minProperties": 1, + "title": "NamespacedOperation", + "type": "object" + }, + "NextOperation": { + "anyOf": [ + { + "type": "string" + }, + { + "properties": { + "builtin": { + "$ref": "#/$defs/BuiltinTarget" + } + }, + "required": [ + "builtin" + ], + "type": "object" + }, + { + "$ref": "#/$defs/NamespacedOperation" + } + ] + }, + "NodeRegistration": { + "properties": { + "config_schema": { + "$ref": "#/$defs/Schema" + }, + "default_display_text": { + "description": "If the user does not specify a default display text, the node ID will\n be used here.", + "type": "string" + }, + "request": { + "type": "string" + }, + "response": { + "type": "string" + } + }, + "required": [ + "default_display_text", + "request", + "response", + "config_schema" + ], + "type": "object" + }, + "NodeSchema": { + "description": "Create an operation that that takes an input message and produces an\n output message.\n\n The behavior is determined by the choice of node `builder` and\n optioanlly the `config` that you provide. Each type of node builder has\n its own schema for the config.\n\n The output message will be sent to the operation specified by `next`.\n\n TODO(@mxgrey): [Support stream outputs](https://github.com/open-rmf/bevy_impulse/issues/43)\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"cutting_board\",\n \"ops\": {\n \"cutting_board\": {\n \"type\": \"node\",\n \"builder\": \"chop\",\n \"config\": \"diced\",\n \"next\": \"bowl\"\n },\n \"bowl\": {\n \"type\": \"node\",\n \"builder\": \"stir\",\n \"next\": \"oven\"\n },\n \"oven\": {\n \"type\": \"node\",\n \"builder\": \"bake\",\n \"config\": {\n \"temperature\": 200,\n \"duration\": 120\n },\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())", + "properties": { + "builder": { + "type": "string" + }, + "config": true, + "display_text": { + "description": "Override for text that should be displayed for an operation within an\n editor.", + "type": [ + "string", + "null" + ] + }, + "next": { + "$ref": "#/$defs/NextOperation" + }, + "stream_out": { + "additionalProperties": { + "$ref": "#/$defs/NextOperation" + }, + "type": "object" + }, + "trace": { + "anyOf": [ + { + "$ref": "#/$defs/TraceToggle" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "builder", + "next" + ], + "type": "object" + }, + "PostRunRequest": { + "properties": { + "diagram": { + "$ref": "#/$defs/Diagram" + }, + "request": true + }, + "required": [ + "diagram", + "request" + ], + "type": "object" + }, + "RetentionPolicy": { + "oneOf": [ + { + "additionalProperties": false, + "description": "Keep the last N items that were stored into the buffer. Once the limit\n is reached, the oldest item will be removed any time a new item arrives.", + "properties": { + "keep_last": { + "format": "uint", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "keep_last" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "Keep the first N items that are stored into the buffer. Once the limit\n is reached, any new item that arrives will be discarded.", + "properties": { + "keep_first": { + "format": "uint", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "keep_first" + ], + "type": "object" + }, + { + "const": "keep_all", + "description": "Do not limit how many items can be stored in the buffer.", + "type": "string" + } + ] + }, + "Schema": { + "type": [ + "object", + "boolean" + ] + }, + "ScopeSchema": { + "description": "Create a scope which will function like its own encapsulated workflow\n within the paren workflow. Each message that enters a scope will trigger\n a new independent session for that scope to begin running with the incoming\n message itself being the input message of the scope. When multiple sessions\n for the same scope are running, they cannot see or interfere with each other.\n\n Once a session terminates, the scope will send the terminating message as\n its output. Scopes can use the `stream_out` operation to stream messages out\n to the parent workflow while running.\n\n Scopes have two common uses:\n * isolate - Prevent simultaneous runs of the same workflow components\n (especially buffers) from interfering with each other.\n * race - Run multiple branches simultaneously inside the scope and race\n them against each ohter. The first branch that reaches the scope's\n terminate operation \"wins\" the race, and only its output will continue\n on in the parent workflow. All other branches will be disposed.\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"approach_door\",\n \"ops\": {\n \"approach_door\": {\n \"type\": \"scope\",\n \"start\": \"begin\",\n \"ops\": {\n \"begin\": {\n \"type\": \"fork_clone\",\n \"next\": [\n \"move_to_door\",\n \"detect_door_proximity\"\n ]\n },\n \"move_to_door\": {\n \"type\": \"node\",\n \"builder\": \"move\",\n \"config\": {\n \"place\": \"L1_north_lobby_outside\"\n },\n \"next\": { \"builtin\" : \"terminate\" }\n },\n \"detect_proximity\": {\n \"type\": \"node\",\n \"builder\": \"detect_proximity\",\n \"config\": {\n \"type\": \"door\",\n \"name\": \"L1_north_lobby\"\n },\n \"next\": { \"builtin\" : \"terminate\" }\n }\n },\n \"next\": { \"builtin\" : \"try_open_door\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```", + "properties": { + "next": { + "$ref": "#/$defs/NextOperation" + }, + "on_implicit_error": { + "anyOf": [ + { + "$ref": "#/$defs/NextOperation" + }, + { + "type": "null" + } + ] + }, + "ops": { + "additionalProperties": { + "$ref": "#/$defs/DiagramOperation" + }, + "description": "Operations that exist inside this scope.", + "type": "object" + }, + "settings": { + "$ref": "#/$defs/ScopeSettings" + }, + "start": { + "$ref": "#/$defs/NextOperation" + }, + "stream_out": { + "additionalProperties": { + "$ref": "#/$defs/NextOperation" + }, + "default": {}, + "description": "Where to connect streams that are coming out of this scope.", + "type": "object" + } + }, + "required": [ + "start", + "ops", + "next" + ], + "type": "object" + }, + "ScopeSettings": { + "description": "Settings which determine how the top-level scope of the workflow behaves.", + "properties": { + "uninterruptible": { + "description": "Should we prevent the scope from being interrupted (e.g. cancelled)?\n False by default, meaning by default scopes can be cancelled or\n interrupted.", + "type": "boolean" + } + }, + "required": [ + "uninterruptible" + ], + "type": "object" + }, + "SectionBuffer": { + "properties": { + "item_type": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "SectionInput": { + "properties": { + "message_type": { + "type": "string" + } + }, + "required": [ + "message_type" + ], + "type": "object" + }, + "SectionMetadata": { + "properties": { + "buffers": { + "additionalProperties": { + "$ref": "#/$defs/SectionBuffer" + }, + "type": "object" + }, + "inputs": { + "additionalProperties": { + "$ref": "#/$defs/SectionInput" + }, + "type": "object" + }, + "outputs": { + "additionalProperties": { + "$ref": "#/$defs/SectionOutput" + }, + "type": "object" + } + }, + "required": [ + "inputs", + "outputs", + "buffers" + ], + "type": "object" + }, + "SectionOutput": { + "properties": { + "message_type": { + "type": "string" + } + }, + "required": [ + "message_type" + ], + "type": "object" + }, + "SectionRegistration": { + "properties": { + "config_schema": { + "$ref": "#/$defs/Schema" + }, + "default_display_text": { + "type": "string" + }, + "metadata": { + "$ref": "#/$defs/SectionMetadata" + } + }, + "required": [ + "default_display_text", + "metadata", + "config_schema" + ], + "type": "object" + }, + "SectionSchema": { + "allOf": [ + { + "oneOf": [ + { + "properties": { + "builder": { + "type": "string" + } + }, + "required": [ + "builder" + ], + "type": "object" + }, + { + "properties": { + "template": { + "type": "string" + } + }, + "required": [ + "template" + ], + "type": "object" + } + ] + }, + { + "description": "Connect the request to a registered section.\n\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"section_op\",\n \"ops\": {\n \"section_op\": {\n \"type\": \"section\",\n \"builder\": \"my_section_builder\",\n \"connect\": {\n \"my_section_output\": { \"builtin\": \"terminate\" }\n }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```\n\n Custom sections can also be created via templates\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"templates\": {\n \"my_template\": {\n \"inputs\": [\"section_input\"],\n \"outputs\": [\"section_output\"],\n \"buffers\": [],\n \"ops\": {\n \"section_input\": {\n \"type\": \"node\",\n \"builder\": \"my_node\",\n \"next\": \"section_output\"\n }\n }\n }\n },\n \"start\": \"section_op\",\n \"ops\": {\n \"section_op\": {\n \"type\": \"section\",\n \"template\": \"my_template\",\n \"connect\": {\n \"section_output\": { \"builtin\": \"terminate\" }\n }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```", + "properties": { + "config": { + "default": null + }, + "connect": { + "additionalProperties": { + "$ref": "#/$defs/NextOperation" + }, + "default": {}, + "type": "object" + }, + "display_text": { + "description": "Override for text that should be displayed for an operation within an\n editor.", + "type": [ + "string", + "null" + ] + }, + "trace": { + "anyOf": [ + { + "$ref": "#/$defs/TraceToggle" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + } + ] + }, + "SectionTemplate": { + "properties": { + "buffers": { + "$ref": "#/$defs/InputRemapping" + }, + "inputs": { + "$ref": "#/$defs/InputRemapping" + }, + "ops": { + "additionalProperties": { + "$ref": "#/$defs/DiagramOperation" + }, + "description": "Operations that define the behavior of the section.", + "type": "object" + }, + "outputs": { + "default": [], + "description": "These are the outputs that the section is exposing so you can connect\n them into siblings of the section.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "ops" + ], + "type": "object" + }, + "SerializedJoinSchema": { + "description": "Same as [`DiagramOperation::Join`] but all input messages must be\n serializable, and the output message will always be [`serde_json::Value`].\n\n If you use an array for `buffers` then the output message will be a\n [`serde_json::Value::Array`]. If you use a map for `buffers` then the\n output message will be a [`serde_json::Value::Object`].\n\n Unlike [`DiagramOperation::Join`], the `target_node` property does not\n exist for this schema.", + "properties": { + "buffers": { + "$ref": "#/$defs/BufferSelection" + }, + "next": { + "$ref": "#/$defs/NextOperation" + } + }, + "required": [ + "next", + "buffers" + ], + "type": "object" + }, + "SplitSchema": { + "description": "If the input message is a list-like or map-like object, split it into\n multiple output messages.\n\n Note that the type of output message from the split depends on how the\n input message implements the [`Splittable`][1] trait. In many cases this\n will be a tuple of `(key, value)`.\n\n There are three ways to specify where the split output messages should\n go, and all can be used at the same time:\n * `sequential` - For array-like collections, send the \"first\" element of\n the collection to the first operation listed in the `sequential` array.\n The \"second\" element of the collection goes to the second operation\n listed in the `sequential` array. And so on for all elements in the\n collection. If one of the elements in the collection is mentioned in\n the `keyed` set, then the sequence will pass over it as if the element\n does not exist at all.\n * `keyed` - For map-like collections, send the split element associated\n with the specified key to its associated output.\n * `remaining` - Any elements that are were not captured by `sequential`\n or by `keyed` will be sent to this.\n\n [1]: crate::Splittable\n\n # Examples\n\n Suppose I am an animal rescuer sorting through a new collection of\n animals that need recuing. My home has space for three exotic animals\n plus any number of dogs and cats.\n\n I have a custom `SpeciesCollection` data structure that implements\n [`Splittable`][1] by allowing you to key on the type of animal.\n\n In the workflow below, we send all cats and dogs to `home`, and we also\n send the first three non-dog and non-cat species to `home`. All\n remaining animals go to the zoo.\n\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"select_animals\",\n \"ops\": {\n \"select_animals\": {\n \"type\": \"split\",\n \"sequential\": [\n \"home\",\n \"home\",\n \"home\"\n ],\n \"keyed\": {\n \"cat\": \"home\",\n \"dog\": \"home\"\n },\n \"remaining\": \"zoo\"\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```\n\n If we input `[\"frog\", \"cat\", \"bear\", \"beaver\", \"dog\", \"rabbit\", \"dog\", \"monkey\"]`\n then `frog`, `bear`, and `beaver` will be sent to `home` since those are\n the first three animals that are not `dog` or `cat`, and we will also\n send one `cat` and two `dog` home. `rabbit` and `monkey` will be sent to the zoo.", + "properties": { + "display_text": { + "description": "Override for text that should be displayed for an operation within an\n editor.", + "type": [ + "string", + "null" + ] + }, + "keyed": { + "additionalProperties": { + "$ref": "#/$defs/NextOperation" + }, + "default": {}, + "type": "object" + }, + "remaining": { + "anyOf": [ + { + "$ref": "#/$defs/NextOperation" + }, + { + "type": "null" + } + ] + }, + "sequential": { + "default": [], + "items": { + "$ref": "#/$defs/NextOperation" + }, + "type": "array" + }, + "trace": { + "anyOf": [ + { + "$ref": "#/$defs/TraceToggle" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "StreamOutSchema": { + "description": "Declare a stream output for the current scope. Outputs that you connect\n to this operation will be streamed out of the scope that this operation\n is declared in.\n\n For the root-level scope, make sure you use a stream pack that is\n compatible with all stream out operations that you declare, otherwise\n you may get a connection error at runtime.\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"plan\",\n \"ops\": {\n \"progress_stream\": {\n \"type\": \"stream_out\",\n \"name\": \"progress\"\n },\n \"plan\": {\n \"type\": \"node\",\n \"builder\": \"planner\",\n \"next\": \"drive\",\n \"stream_out\" : {\n \"progress\": \"progress_stream\"\n }\n },\n \"drive\": {\n \"type\": \"node\",\n \"builder\": \"navigation\",\n \"next\": { \"builtin\": \"terminate\" },\n \"stream_out\": {\n \"progress\": \"progress_stream\"\n }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```", + "properties": { + "name": { + "description": "The name of the stream exiting the workflow or scope.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "TraceToggle": { + "oneOf": [ + { + "const": "off", + "description": "Do not emit any signal when the operation is activated.", + "type": "string" + }, + { + "const": "on", + "description": "Emit a minimal signal with just the operation information when the\n operation is activated.", + "type": "string" + }, + { + "const": "messages", + "description": "Emit a signal that includes a serialized copy of the message when the\n operation is activated. This may substantially increase the overhead of\n triggering operations depending on the size and frequency of the messages,\n so it is recommended only for high-level workflows or for debugging.\n\n If the message is not serializable then it will simply not be included\n in the event information.", + "type": "string" + } + ] + }, + "TransformSchema": { + "description": "If the request is serializable, transform it by running it through a [CEL](https://cel.dev/) program.\n The context includes a \"request\" variable which contains the input message.\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"transform\",\n \"ops\": {\n \"transform\": {\n \"type\": \"transform\",\n \"cel\": \"request.name\",\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```\n\n Note that due to how `serde_json` performs serialization, positive integers are always\n serialized as unsigned. In CEL, You can't do an operation between unsigned and signed so\n it is recommended to always perform explicit casts.\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"transform\",\n \"ops\": {\n \"transform\": {\n \"type\": \"transform\",\n \"cel\": \"int(request.score) * 3\",\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```", + "properties": { + "cel": { + "type": "string" + }, + "display_text": { + "description": "Override for text that should be displayed for an operation within an\n editor.", + "type": [ + "string", + "null" + ] + }, + "next": { + "$ref": "#/$defs/NextOperation" + }, + "on_error": { + "anyOf": [ + { + "$ref": "#/$defs/NextOperation" + }, + { + "type": "null" + } + ] + }, + "trace": { + "anyOf": [ + { + "$ref": "#/$defs/TraceToggle" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "cel", + "next" + ], + "type": "object" + }, + "UnzipSchema": { + "description": "If the input message is a tuple of (T1, T2, T3, ...), unzip it into\n multiple output messages of T1, T2, T3, ...\n\n Each output message may have a different type and can be sent to a\n different operation. This creates multiple simultaneous branches of\n execution within the workflow. See [`DiagramOperation::ForkClone`] for\n more information on parallel branches.\n\n # Examples\n ```\n # bevy_impulse::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"name_phone_address\",\n \"ops\": {\n \"name_phone_address\": {\n \"type\": \"unzip\",\n \"next\": [\n \"process_name\",\n \"process_phone_number\",\n \"process_address\"\n ]\n },\n \"process_name\": {\n \"type\": \"node\",\n \"builder\": \"process_name\",\n \"next\": \"name_processed\"\n },\n \"process_phone_number\": {\n \"type\": \"node\",\n \"builder\": \"process_phone_number\",\n \"next\": \"phone_number_processed\"\n },\n \"process_address\": {\n \"type\": \"node\",\n \"builder\": \"process_address\",\n \"next\": \"address_processed\"\n },\n \"name_processed\": { \"type\": \"buffer\" },\n \"phone_number_processed\": { \"type\": \"buffer\" },\n \"address_processed\": { \"type\": \"buffer\" },\n \"finished\": {\n \"type\": \"join\",\n \"buffers\": [\n \"name_processed\",\n \"phone_number_processed\",\n \"address_processed\"\n ],\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())\n ```", + "properties": { + "display_text": { + "description": "Override for text that should be displayed for an operation within an\n editor.", + "type": [ + "string", + "null" + ] + }, + "next": { + "items": { + "$ref": "#/$defs/NextOperation" + }, + "type": "array" + }, + "trace": { + "anyOf": [ + { + "$ref": "#/$defs/TraceToggle" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "next" + ], + "type": "object" + } + } +} \ No newline at end of file diff --git a/diagram-editor/frontend/app.css b/diagram-editor/frontend/app.css new file mode 100644 index 00000000..3cdf9ce0 --- /dev/null +++ b/diagram-editor/frontend/app.css @@ -0,0 +1,47 @@ +/* reactflow overrides */ +/* see node_modules/@xyflow/react/dist/style.css */ +.react-flow.dark { + --xy-edge-stroke-default: var(--mui-palette-grey-700); +} +.react-flow__pane.draggable { + cursor: default; +} +.react-flow__pane.dragging { + cursor: grabbing; +} +.react-flow__handle { + width: 8px; + height: 8px; + z-index: 10; +} +.react-flow__handle::before { + content: ""; + position: absolute; + width: 24px; + height: 24px; + left: -150%; + top: -100%; +} +.react-flow__handle.handle-data { + background-color: var(--xy-handle-background-color-default); +} +.react-flow__handle.handle-buffer { + background-color: var(--mui-palette-secondary-dark); +} +.react-flow__handle.handle-stream { + background-color: var(--mui-palette-warning-dark); +} +.react-flow__handle.handle-data-buffer { + background: linear-gradient( + -45deg, + var(--mui-palette-secondary-dark) 50%, + var(--xy-handle-background-color-default) 50% + ); +} +.react-flow__handle.handle-data-stream { + background: linear-gradient( + -45deg, + var(--mui-palette-warning-dark) 50%, + var(--xy-handle-background-color-default) 50% + ); +} diff --git a/diagram-editor/frontend/app.tsx b/diagram-editor/frontend/app.tsx new file mode 100644 index 00000000..30f3507f --- /dev/null +++ b/diagram-editor/frontend/app.tsx @@ -0,0 +1,35 @@ +import '@fontsource/roboto/300.css'; +import '@fontsource/roboto/400.css'; +import '@fontsource/roboto/500.css'; +import '@fontsource/roboto/700.css'; +import '@xyflow/react/dist/style.css'; +import { CssBaseline, createTheme, ThemeProvider } from '@mui/material'; + +import './app.css'; +import DiagramEditor from './diagram-editor'; +import { RegistryProvider } from './registry-provider'; +import { TemplatesProvider } from './templates-provider'; + +const theme = createTheme({ + palette: { + mode: 'dark', + }, + cssVariables: true, +}); + +const App = () => { + return ( + + +
+ + + + + +
+
+ ); +}; + +export default App; diff --git a/diagram-editor/frontend/auto-layout-button.tsx b/diagram-editor/frontend/auto-layout-button.tsx new file mode 100644 index 00000000..dc39880c --- /dev/null +++ b/diagram-editor/frontend/auto-layout-button.tsx @@ -0,0 +1,35 @@ +import { Button, Tooltip } from '@mui/material'; +import { type NodeChange, type ReactFlowState, useStore } from '@xyflow/react'; +import React from 'react'; +import type { DiagramEditorEdge } from './edges'; +import type { DiagramEditorNode } from './nodes'; +import { MaterialSymbol } from './nodes'; +import { autoLayout } from './utils/auto-layout'; +import { LAYOUT_OPTIONS } from './utils/layout'; + +export interface AutoLayoutButtonProps { + onNodeChanges: (changes: NodeChange[]) => void; +} + +const nodesEdgesSelector = (state: ReactFlowState) => ({ + nodes: state.nodes as DiagramEditorNode[], + edges: state.edges as DiagramEditorEdge[], +}); + +function AutoLayoutButton({ onNodeChanges }: AutoLayoutButtonProps) { + const { nodes, edges } = useStore(nodesEdgesSelector); + return ( + + + + ); +} + +export default React.memo(AutoLayoutButton); diff --git a/diagram-editor/frontend/command-panel.tsx b/diagram-editor/frontend/command-panel.tsx new file mode 100644 index 00000000..5789ec41 --- /dev/null +++ b/diagram-editor/frontend/command-panel.tsx @@ -0,0 +1,91 @@ +import { Button, ButtonGroup, styled, Tooltip } from '@mui/material'; +import { type NodeChange, Panel } from '@xyflow/react'; +import React from 'react'; +import AutoLayoutButton from './auto-layout-button'; +import EditTemplatesDialog from './edit-templates-dialog'; +import { EditorMode, useEditorMode } from './editor-mode'; +import type { DiagramEditorNode } from './nodes'; +import { MaterialSymbol } from './nodes'; +import { RunButton } from './run-button'; + +export interface CommandPanelProps { + onNodeChanges: (changes: NodeChange[]) => void; + onExportClick: () => void; + onLoadDiagram: (jsonStr: string) => void; +} + +const VisuallyHiddenInput = styled('input')({ + clip: 'rect(0 0 0 0)', + clipPath: 'inset(50%)', + height: 1, + overflow: 'hidden', + position: 'absolute', + bottom: 0, + left: 0, + whiteSpace: 'nowrap', + width: 1, +}); + +function CommandPanel({ + onNodeChanges, + onExportClick, + onLoadDiagram, +}: CommandPanelProps) { + const [openEditTemplatesDialog, setOpenEditTemplatesDialog] = + React.useState(false); + const [editorMode] = useEditorMode(); + + return ( + <> + + + {editorMode.mode === EditorMode.Normal && } + {editorMode.mode === EditorMode.Normal && ( + + + + )} + + {editorMode.mode === EditorMode.Normal && ( + + + + )} + {editorMode.mode === EditorMode.Normal && ( + + {/* biome-ignore lint/a11y/useValidAriaRole: button used as a label, should have no role */} + + + )} + + + setOpenEditTemplatesDialog(false)} + /> + + ); +} + +export default React.memo(CommandPanel); diff --git a/diagram-editor/frontend/diagram-editor.tsx b/diagram-editor/frontend/diagram-editor.tsx new file mode 100644 index 00000000..ed58b72f --- /dev/null +++ b/diagram-editor/frontend/diagram-editor.tsx @@ -0,0 +1,810 @@ +import { + Alert, + alpha, + darken, + Fab, + Popover, + type PopoverPosition, + type PopoverProps, + Snackbar, + Typography, + useTheme, +} from '@mui/material'; +import { + addEdge, + applyEdgeChanges, + applyNodeChanges, + Background, + type Connection, + type EdgeChange, + type EdgeRemoveChange, + type NodeChange, + type NodeRemoveChange, + Panel, + ReactFlow, + type ReactFlowInstance, + reconnectEdge, + type XYPosition, +} from '@xyflow/react'; +import { inflateSync, strFromU8 } from 'fflate'; +import React from 'react'; +import AddOperation from './add-operation'; +import CommandPanel from './command-panel'; +import type { DiagramEditorEdge } from './edges'; +import { + createBaseEdge, + EDGE_CATEGORIES, + EDGE_TYPES, + EdgeCategory, +} from './edges'; +import { + EditorMode, + type EditorModeContext, + EditorModeProvider, + type UseEditorModeContext, +} from './editor-mode'; +import ExportDiagramDialog from './export-diagram-dialog'; +import { defaultEdgeData, EditEdgeForm, EditNodeForm } from './forms'; +import EditScopeForm from './forms/edit-scope-form'; +import type { HandleId } from './handles'; +import { NodeManager, NodeManagerProvider } from './node-manager'; +import { + type DiagramEditorNode, + isBuiltinNode, + MaterialSymbol, + NODE_TYPES, + type OperationNode, +} from './nodes'; +import { useTemplates } from './templates-provider'; +import { EdgesProvider } from './use-edges'; +import { autoLayout } from './utils/auto-layout'; +import { isRemoveChange } from './utils/change'; +import { getValidEdgeTypes, validateEdgeSimple } from './utils/connection'; +import { exhaustiveCheck } from './utils/exhaustive-check'; +import { exportTemplate } from './utils/export-diagram'; +import { calculateScopeBounds, LAYOUT_OPTIONS } from './utils/layout'; +import { loadDiagramJson, loadEmpty, loadTemplate } from './utils/load-diagram'; + +const NonCapturingPopoverContainer = ({ + children, +}: { + children: React.ReactNode; +}) => <>{children}; + +interface EditingEdge { + sourceNode: DiagramEditorNode; + targetNode: DiagramEditorNode; + edge: DiagramEditorEdge; +} + +/** + * Given a node change, get the parent id and the new position if the change would be applied. + * Returns null if the change does not result in any position changes. + */ +function getChangeParentIdAndPosition( + nodeManager: NodeManager, + change: NodeChange, +): [string, string | null, XYPosition] | null { + switch (change.type) { + case 'position': { + const changedNode = nodeManager.tryGetNode(change.id); + if (!changedNode) { + return null; + } + return change.position + ? [change.id, changedNode.parentId || null, change.position] + : null; + } + case 'add': + case 'replace': { + return [ + change.item.id, + change.item.parentId || null, + change.item.position, + ]; + } + default: { + return null; + } + } +} + +interface ProvidersProps { + editorModeContext: UseEditorModeContext; + nodeManager: NodeManager; + edges: DiagramEditorEdge[]; +} + +function Providers({ + editorModeContext, + nodeManager, + edges, + children, +}: React.PropsWithChildren) { + return ( + + + {children} + + + ); +} + +function DiagramEditor() { + const reactFlowInstance = React.useRef | null>(null); + + const [editorMode, setEditorMode] = React.useState({ + mode: EditorMode.Normal, + }); + + const [nodes, setNodes] = React.useState( + () => loadEmpty().nodes, + ); + const nodeManager = React.useMemo(() => new NodeManager(nodes), [nodes]); + const savedNodes = React.useRef([]); + + const [edges, setEdges] = React.useState([]); + const savedEdges = React.useRef([]); + + const [templates] = useTemplates(); + + const updateEditorModeAction = React.useCallback( + (newMode: EditorModeContext) => { + switch (newMode.mode) { + case EditorMode.Normal: { + setNodes([...savedNodes.current]); + setEdges([...savedEdges.current]); + reactFlowInstance.current?.fitView(); + break; + } + case EditorMode.Template: { + const template = templates[newMode.templateId]; + if (!template) { + throw new Error(`template ${newMode.templateId} not found`); + } + const graph = loadTemplate(template); + const changes = autoLayout(graph.nodes, graph.edges, LAYOUT_OPTIONS); + // using callback form so that `nodes` and `edges` don't need to be part of dependencies. + setNodes((prev) => { + savedNodes.current = [...prev]; + return applyNodeChanges(changes, graph.nodes); + }); + setEdges((prev) => { + savedEdges.current = [...prev]; + return graph.edges; + }); + reactFlowInstance.current?.fitView(); + break; + } + default: { + exhaustiveCheck(newMode); + throw new Error('unknown editor mode'); + } + } + + setEditorMode(newMode); + }, + [templates], + ); + + const handleEdgeChanges = React.useCallback( + (changes: EdgeChange[]) => { + setEdges((prev) => applyEdgeChanges(changes, prev)); + }, + [], + ); + + const handleEdgeChange = React.useCallback( + (change: EdgeChange) => { + handleEdgeChanges([change]); + }, + [handleEdgeChanges], + ); + + const [_, setTemplates] = useTemplates(); + + const theme = useTheme(); + + const backgroundColor = React.useMemo(() => { + switch (editorMode.mode) { + case EditorMode.Normal: + return theme.palette.background.default; + case EditorMode.Template: + return darken(theme.palette.primary.main, 0.8); + default: + exhaustiveCheck(editorMode); + throw new Error('unknown editor mode'); + } + }, [editorMode, theme]); + + const handleNodeChanges = React.useCallback( + (changes: NodeChange[]) => { + const transitiveChanges: NodeChange[] = []; + + // resize and reposition scope + for (const change of changes) { + const changeIdPos = getChangeParentIdAndPosition(nodeManager, change); + if (!changeIdPos) { + continue; + } + const [changeId, changeParentId, changePosition] = changeIdPos; + if (!changeParentId) { + continue; + } + + const scopeNode = nodeManager.tryGetNode(changeParentId); + if (!scopeNode) { + continue; + } + const scopeChildren = nodeManager.nodes.filter( + (n) => n.parentId === changeParentId && n.id !== changeId, + ); + const calculatedBounds = calculateScopeBounds([ + ...scopeChildren.map((n) => n.position), + { + // react flow does some kind of rounding (or maybe it is due to floating point accuracies) + // that results in gitches when resizing a scope quickly. This rounding reduces the + // impact of the glitches. + x: Math.round(changePosition.x), + y: Math.round(changePosition.y), + }, + ]); + + const newScopeBounds = { + x: scopeNode.position.x, + y: scopeNode.position.y, + width: scopeNode.width ?? calculatedBounds.width, + height: scopeNode.height ?? calculatedBounds.height, + }; + // React Flow cannot handle fast changing of a parent's position while changing the + // children's position as well (some kind of race condition that causes the node positions to jump around). + // Workaround by resizing the scope only if it hits a certain threshold. + if ( + Math.abs(calculatedBounds.x) > + LAYOUT_OPTIONS.scopePadding.leftRight || + Math.abs(calculatedBounds.y) > + LAYOUT_OPTIONS.scopePadding.topBottom || + (scopeNode.width && + Math.abs(calculatedBounds.width - scopeNode.width) > + LAYOUT_OPTIONS.scopePadding.leftRight) || + (scopeNode.height && + Math.abs(calculatedBounds.height - scopeNode.height) > + LAYOUT_OPTIONS.scopePadding.topBottom) + ) { + newScopeBounds.x += calculatedBounds.x; + newScopeBounds.width = calculatedBounds.width; + newScopeBounds.y += calculatedBounds.y; + newScopeBounds.height = calculatedBounds.height; + } + + if ( + newScopeBounds.x !== scopeNode.position.x || + newScopeBounds.y !== scopeNode.position.y || + newScopeBounds.width !== scopeNode.width || + newScopeBounds.height !== scopeNode.height + ) { + transitiveChanges.push({ + type: 'position', + id: changeParentId, + position: { + x: newScopeBounds.x, + y: newScopeBounds.y, + }, + }); + transitiveChanges.push({ + type: 'dimensions', + id: changeParentId, + dimensions: { + width: newScopeBounds.width, + height: newScopeBounds.height, + }, + setAttributes: true, + }); + // when the scope is moved, the relative position of the nodes will change so we + // need to update them to keep them in place. + for (const child of scopeChildren) { + transitiveChanges.push({ + type: 'position', + id: child.id, + position: { + x: child.position.x - calculatedBounds.x, + y: child.position.y - calculatedBounds.y, + }, + }); + } + } + } + + // remove children nodes of a removed parent + const removedNodes = new Set( + changes + .filter((change) => isRemoveChange(change)) + .map((change) => change.id), + ); + while (true) { + let newChanges = false; + for (const node of nodeManager.nodes) { + if ( + node.parentId && + removedNodes.has(node.parentId) && + !removedNodes.has(node.id) + ) { + transitiveChanges.push({ + type: 'remove', + id: node.id, + }); + removedNodes.add(node.id); + newChanges = true; + } + } + if (!newChanges) { + break; + } + } + + // clean up dangling edges when a node is removed. + const edgeChanges: EdgeRemoveChange[] = []; + for (const edge of edges) { + if (removedNodes.has(edge.source) || removedNodes.has(edge.target)) { + edgeChanges.push({ + type: 'remove', + id: edge.id, + }); + } + } + handleEdgeChanges(edgeChanges); + + setNodes((prev) => + applyNodeChanges([...changes, ...transitiveChanges], prev), + ); + }, + [handleEdgeChanges, nodeManager, edges], + ); + + const handleNodeChange = React.useCallback( + (change: NodeChange) => { + handleNodeChanges([change]); + }, + [handleNodeChanges], + ); + + const [addOperationPopover, setAddOperationPopover] = React.useState<{ + open: boolean; + popOverPosition: PopoverPosition; + parentId: string | null; + }>({ + open: false, + popOverPosition: { left: 0, top: 0 }, + parentId: null, + }); + const addOperationNewNodePosition = React.useMemo(() => { + if (!reactFlowInstance.current) { + return { x: 0, y: 0 }; + } + const parentNode = addOperationPopover.parentId + ? nodeManager.tryGetNode(addOperationPopover.parentId) + : null; + + const parentPosition = parentNode?.position + ? parentNode.position + : { x: 0, y: 0 }; + return ( + reactFlowInstance.current?.screenToFlowPosition({ + x: addOperationPopover.popOverPosition.left - parentPosition.x, + y: addOperationPopover.popOverPosition.top - parentPosition.y, + }) || { x: 0, y: 0 } + ); + }, [ + nodeManager, + addOperationPopover.parentId, + addOperationPopover.popOverPosition, + ]); + + const [editingNodeId, setEditingNodeId] = React.useState(null); + + const [editingEdgeId, setEditingEdgeId] = React.useState(null); + const editingEdge: EditingEdge | null = (() => { + if (!editingEdgeId) { + return null; + } + + const edge = edges.find((e) => e.id === editingEdgeId); + if (!edge) { + console.error(`cannot find edge ${editingEdgeId}`); + return null; + } + + const sourceNode = nodeManager.tryGetNode(edge.source); + if (!sourceNode) { + console.error(`cannot find node ${edge.source}`); + return null; + } + const targetNode = nodeManager.tryGetNode(edge.target); + if (!targetNode) { + console.error(`cannot find node ${edge.target}`); + return null; + } + + return { + edge, + sourceNode, + targetNode, + }; + })(); + + const closeAllPopovers = React.useCallback(() => { + setEditingNodeId(null); + setEditingEdgeId(null); + setAddOperationPopover((prev) => ({ ...prev, open: false })); + setEditOpFormPopoverProps({ open: false }); + }, []); + + const [editOpFormPopoverProps, setEditOpFormPopoverProps] = React.useState< + Pick< + PopoverProps, + 'open' | 'anchorReference' | 'anchorEl' | 'anchorPosition' + > + >({ open: false }); + const renderEditForm = React.useCallback( + (nodeId: string) => { + const node = nodeManager.tryGetNode(nodeId); + if (!node || isBuiltinNode(node)) { + return null; + } + + const handleDelete = (change: NodeRemoveChange) => { + handleNodeChange(change); + closeAllPopovers(); + }; + + if (node.type === 'scope') { + return ( + } + onChange={handleNodeChange} + onDelete={handleDelete} + onAddOperationClick={(ev) => { + setAddOperationPopover({ + open: true, + popOverPosition: { left: ev.clientX, top: ev.clientY }, + parentId: node.id, + }); + }} + /> + ); + } + return ( + + ); + }, + [nodeManager, editingNodeId, handleNodeChange, closeAllPopovers], + ); + + const mouseDownTime = React.useRef(0); + + const [errorToast, setErrorToast] = React.useState(null); + const [openErrorToast, setOpenErrorToast] = React.useState(false); + const showErrorToast = React.useCallback((message: string) => { + setErrorToast(message); + setOpenErrorToast(true); + }, []); + + const loadDiagram = React.useCallback( + (jsonStr: string) => { + try { + const [diagram, graph] = loadDiagramJson(jsonStr); + const changes = autoLayout(graph.nodes, graph.edges, LAYOUT_OPTIONS); + setNodes(applyNodeChanges(changes, graph.nodes)); + setEdges(graph.edges); + setTemplates(diagram.templates || {}); + reactFlowInstance.current?.fitView(); + closeAllPopovers(); + } catch (e) { + showErrorToast(`failed to load diagram: ${e}`); + } + }, + [closeAllPopovers, showErrorToast], + ); + + const [openExportDiagramDialog, setOpenExportDiagramDialog] = + React.useState(false); + + const handleMouseDown = React.useCallback(() => { + mouseDownTime.current = Date.now(); + }, []); + + const tryCreateEdge = React.useCallback( + (conn: Connection, id?: string): DiagramEditorEdge | null => { + const sourceNode = nodeManager.tryGetNode(conn.source); + const targetNode = nodeManager.tryGetNode(conn.target); + if (!sourceNode || !targetNode) { + throw new Error('cannot find source or target node'); + } + + const validEdges = getValidEdgeTypes( + sourceNode, + conn.sourceHandle as HandleId, + targetNode, + conn.targetHandle as HandleId, + ); + if (validEdges.length === 0) { + showErrorToast( + `cannot connect "${sourceNode.type}" to "${targetNode.type}"`, + ); + return null; + } + + const newEdge = { + ...createBaseEdge(conn.source, conn.target, id), + type: validEdges[0], + data: defaultEdgeData(validEdges[0]), + } as DiagramEditorEdge; + + if (targetNode.type === 'section') { + if (EDGE_CATEGORIES[newEdge.type] === EdgeCategory.Buffer) { + newEdge.data.input = { + type: 'sectionBuffer', + inputId: '', + }; + } else if (EDGE_CATEGORIES[newEdge.type] === EdgeCategory.Data) { + newEdge.data.input = { + type: 'sectionInput', + inputId: '', + }; + } + } + + const validationResult = validateEdgeSimple(newEdge, nodeManager, edges); + if (!validationResult.valid) { + showErrorToast(validationResult.error); + return null; + } + + return newEdge; + }, + [showErrorToast, nodeManager, edges], + ); + + return ( + + { + reactFlowInstance.current = instance; + + const queryParams = new URLSearchParams(window.location.search); + const diagramParam = queryParams.get('diagram'); + + if (!diagramParam) { + return; + } + + try { + const binaryString = atob(diagramParam); + const byteArray = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + byteArray[i] = binaryString.charCodeAt(i); + } + const diagramJson = strFromU8(inflateSync(byteArray)); + loadDiagram(diagramJson); + } catch (e) { + if (e instanceof Error) { + showErrorToast(`failed to load diagram: ${e.message}`); + } else { + throw e; + } + } + }} + onNodesChange={handleNodeChanges} + onNodesDelete={() => { + closeAllPopovers(); + }} + onEdgesChange={handleEdgeChanges} + onEdgesDelete={() => { + closeAllPopovers(); + }} + onConnect={(conn) => { + const newEdge = tryCreateEdge(conn); + if (newEdge) { + setEdges((prev) => addEdge(newEdge, prev)); + } + }} + isValidConnection={(conn) => { + const sourceNode = nodeManager.tryGetNode(conn.source); + const targetNode = nodeManager.tryGetNode(conn.target); + if (!sourceNode || !targetNode) { + throw new Error('cannot find source or target node'); + } + + const allowedEdges = getValidEdgeTypes( + sourceNode, + conn.sourceHandle as HandleId, + targetNode, + conn.targetHandle as HandleId, + ); + return allowedEdges.length > 0; + }} + onReconnect={(oldEdge, newConnection) => { + const newEdge = tryCreateEdge(newConnection, oldEdge.id); + if (newEdge) { + oldEdge.type = newEdge.type; + oldEdge.data = newEdge.data; + setEdges((prev) => reconnectEdge(oldEdge, newConnection, prev)); + } + }} + onNodeClick={(ev, node) => { + ev.stopPropagation(); + closeAllPopovers(); + + if (isBuiltinNode(node)) { + return; + } + setEditingNodeId(node.id); + + setEditOpFormPopoverProps({ + open: true, + anchorReference: 'anchorPosition', + anchorPosition: { left: ev.clientX, top: ev.clientY }, + }); + }} + onEdgeClick={(ev, edge) => { + ev.stopPropagation(); + closeAllPopovers(); + + const sourceNode = nodes.find((n) => n.id === edge.source); + const targetNode = nodes.find((n) => n.id === edge.target); + if (!sourceNode || !targetNode) { + throw new Error('unable to find source or target node'); + } + + setEditingEdgeId(edge.id); + + setEditOpFormPopoverProps({ + open: true, + anchorReference: 'anchorPosition', + anchorPosition: { left: ev.clientX, top: ev.clientY }, + }); + }} + onPaneClick={(ev) => { + if (addOperationPopover.open || editOpFormPopoverProps.open) { + closeAllPopovers(); + return; + } + + // filter out erroneous click after connecting an edge + const now = Date.now(); + if (now - mouseDownTime.current > 200) { + return; + } + setAddOperationPopover({ + open: true, + popOverPosition: { left: ev.clientX, top: ev.clientY }, + parentId: null, + }); + }} + onMouseDownCapture={handleMouseDown} + onTouchStartCapture={handleMouseDown} + colorMode="dark" + deleteKeyCode={'Delete'} + > + + {editorMode.mode === EditorMode.Template && ( + + {editorMode.templateId} + + )} + setOpenExportDiagramDialog(true), + [], + )} + onLoadDiagram={loadDiagram} + /> + {editorMode.mode === EditorMode.Template && ( + { + const exportedTemplate = exportTemplate(nodeManager, edges); + setTemplates((prev) => ({ + ...prev, + [editorMode.templateId]: exportedTemplate, + })); + updateEditorModeAction({ mode: EditorMode.Normal }); + }} + > + + + )} + + setAddOperationPopover((prev) => ({ ...prev, open: false })) + } + anchorReference="anchorPosition" + anchorPosition={addOperationPopover.popOverPosition} + // use a custom component to prevent the popover from creating an invisible element that blocks clicks + component={NonCapturingPopoverContainer} + > + { + handleNodeChanges(changes); + closeAllPopovers(); + }} + /> + + setEditOpFormPopoverProps({ open: false })} + anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} + // use a custom component to prevent the popover from creating an invisible element that blocks clicks + component={NonCapturingPopoverContainer} + > + {editingNodeId && renderEditForm(editingNodeId)} + {editingEdge && ( + { + setEdges((prev) => applyEdgeChanges([changes], prev)); + closeAllPopovers(); + }} + /> + )} + + { + if (reason === 'clickaway') { + return; + } + setOpenErrorToast(false); + }} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + setOpenErrorToast(false)} severity="error"> + {errorToast} + + + setOpenExportDiagramDialog(false)} + /> + + + ); +} + +export default DiagramEditor; diff --git a/diagram-editor/frontend/edges/buffer-edge.tsx b/diagram-editor/frontend/edges/buffer-edge.tsx new file mode 100644 index 00000000..d72084c1 --- /dev/null +++ b/diagram-editor/frontend/edges/buffer-edge.tsx @@ -0,0 +1,36 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { Edge } from '../types/react-flow'; +import type { + BufferKeyInputSlotData, + BufferSeqInputSlotData, + SectionBufferInputSlotData, +} from './input-slots'; + +export type BufferOutputData = Record; + +export type BufferEdge = Edge< + BufferOutputData, + BufferKeyInputSlotData | BufferSeqInputSlotData | SectionBufferInputSlotData, + 'buffer' +>; + +export type BufferEdgeCompProps = Exclude, 'label'>; + +export const BufferEdgeComp = memo((props: BufferEdgeCompProps) => { + const label = (() => { + switch (props.data.input.type) { + case 'bufferKey': { + return props.data.input.key || 'Select Key'; + } + case 'bufferSeq': { + return props.data.input.seq.toString(); + } + case 'sectionBuffer': { + return props.data.input.inputId || 'Select Buffer'; + } + } + })(); + + return ; +}); diff --git a/diagram-editor/frontend/edges/create-edge.ts b/diagram-editor/frontend/edges/create-edge.ts new file mode 100644 index 00000000..9f78662b --- /dev/null +++ b/diagram-editor/frontend/edges/create-edge.ts @@ -0,0 +1,149 @@ +import { MarkerType } from '@xyflow/react'; +import { v4 as uuidv4 } from 'uuid'; +import type { DiagramEditorEdge, EdgeOutputData } from '.'; +import type { DefaultEdge } from './default-edge'; +import type { + BufferKeyInputSlotData, + BufferSeqInputSlotData, + SectionBufferInputSlotData, +} from './input-slots'; + +export function createBaseEdge( + source: string, + target: string, + id?: string, +): Pick { + return { + id: id || uuidv4(), + source, + target, + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20, + }, + }; +} + +export function createDefaultEdge( + source: string, + target: string, + inputSlot?: DefaultEdge['data']['input'], +): DiagramEditorEdge<'default'> { + return { + ...createBaseEdge(source, target), + type: 'default', + data: { output: {}, input: inputSlot || { type: 'default' } }, + }; +} + +export function createUnzipEdge( + source: string, + target: string, + data: EdgeOutputData<'unzip'>, +): DiagramEditorEdge<'unzip'> { + return { + ...createBaseEdge(source, target), + type: 'unzip', + data: { + output: data, + input: { type: 'default' }, + }, + }; +} + +export function createForkResultOkEdge( + source: string, + target: string, +): DiagramEditorEdge<'forkResultOk'> { + return { + ...createBaseEdge(source, target), + type: 'forkResultOk', + data: { output: {}, input: { type: 'default' } }, + }; +} + +export function createForkResultErrEdge( + source: string, + target: string, +): DiagramEditorEdge<'forkResultErr'> { + return { + ...createBaseEdge(source, target), + type: 'forkResultErr', + data: { output: {}, input: { type: 'default' } }, + }; +} + +export function createSplitKeyEdge( + source: string, + target: string, + data: EdgeOutputData<'splitKey'>, +): DiagramEditorEdge<'splitKey'> { + return { + ...createBaseEdge(source, target), + type: 'splitKey', + data: { output: data, input: { type: 'default' } }, + }; +} + +export function createSplitSeqEdge( + source: string, + target: string, + data: EdgeOutputData<'splitSeq'>, +): DiagramEditorEdge<'splitSeq'> { + return { + ...createBaseEdge(source, target), + type: 'splitSeq', + data: { output: data, input: { type: 'default' } }, + }; +} + +export function createSplitRemainingEdge( + source: string, + target: string, +): DiagramEditorEdge<'splitRemaining'> { + return { + ...createBaseEdge(source, target), + type: 'splitRemaining', + data: { output: {}, input: { type: 'default' } }, + }; +} + +export function createBufferEdge( + source: string, + target: string, + data: + | BufferKeyInputSlotData + | BufferSeqInputSlotData + | SectionBufferInputSlotData, +): DiagramEditorEdge<'buffer'> { + return { + ...createBaseEdge(source, target), + type: 'buffer', + data: { output: {}, input: data }, + }; +} + +export function createStreamOutEdge( + source: string, + target: string, + data: EdgeOutputData<'streamOut'>, +): DiagramEditorEdge<'streamOut'> { + return { + ...createBaseEdge(source, target), + type: 'streamOut', + data: { output: data, input: { type: 'default' } }, + }; +} + +export function createSectionEdge( + source: string, + target: string, + data: EdgeOutputData<'section'>, +): DiagramEditorEdge<'section'> { + return { + ...createBaseEdge(source, target), + type: 'section', + data: { output: data, input: { type: 'default' } }, + }; +} diff --git a/diagram-editor/frontend/edges/data-edge.ts b/diagram-editor/frontend/edges/data-edge.ts new file mode 100644 index 00000000..91fe10ba --- /dev/null +++ b/diagram-editor/frontend/edges/data-edge.ts @@ -0,0 +1,10 @@ +import type { Edge } from '../types/react-flow'; +import type { DefaultInputSlotData, SectionInputSlotData } from './input-slots'; + +/** + * A specialization of `Edge` that enforces the edge input slot data is valid for data edges. + */ +export type DataEdge< + O extends Record, + S extends string, +> = Edge; diff --git a/diagram-editor/frontend/edges/default-edge.tsx b/diagram-editor/frontend/edges/default-edge.tsx new file mode 100644 index 00000000..9f585736 --- /dev/null +++ b/diagram-editor/frontend/edges/default-edge.tsx @@ -0,0 +1,22 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { DataEdge } from './data-edge'; + +export type DefaultEdgeOutputData = Record; + +export type DefaultEdge = DataEdge; + +export type DefaultEdgeCompProps = Omit, 'label'>; + +export const DefaultEdgeComp = memo((props: DefaultEdgeCompProps) => { + return ( + + ); +}); diff --git a/diagram-editor/frontend/edges/fork-result-err-edge.tsx b/diagram-editor/frontend/edges/fork-result-err-edge.tsx new file mode 100644 index 00000000..f774123e --- /dev/null +++ b/diagram-editor/frontend/edges/fork-result-err-edge.tsx @@ -0,0 +1,20 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { DataEdge } from './data-edge'; + +export type ForkResultErrOutputData = Record; +export type ForkResultErrEdge = DataEdge< + ForkResultErrOutputData, + 'forkResultErr' +>; + +export type ForkResultErrEdgeProps = Exclude< + EdgeProps, + 'label' +>; + +function ForkResultErrEdgeComp(props: ForkResultErrEdgeProps) { + return ; +} + +export default memo(ForkResultErrEdgeComp); diff --git a/diagram-editor/frontend/edges/fork-result-ok-edge.tsx b/diagram-editor/frontend/edges/fork-result-ok-edge.tsx new file mode 100644 index 00000000..d252cab6 --- /dev/null +++ b/diagram-editor/frontend/edges/fork-result-ok-edge.tsx @@ -0,0 +1,17 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { DataEdge } from './data-edge'; + +export type ForkResultOkOutputData = Record; +export type ForkResultOkEdge = DataEdge; + +export type ForkResultOkEdgeProps = Exclude< + EdgeProps, + 'label' +>; + +function ForkResultOkEdgeComp(props: ForkResultOkEdgeProps) { + return ; +} + +export default memo(ForkResultOkEdgeComp); diff --git a/diagram-editor/frontend/edges/index.ts b/diagram-editor/frontend/edges/index.ts new file mode 100644 index 00000000..ba2f8b79 --- /dev/null +++ b/diagram-editor/frontend/edges/index.ts @@ -0,0 +1,96 @@ +import { type BufferEdge, BufferEdgeComp } from './buffer-edge'; +import type { DataEdge } from './data-edge'; +import { type DefaultEdge, DefaultEdgeComp } from './default-edge'; +import ForkResultErrEdgeComp, { + type ForkResultErrEdge, +} from './fork-result-err-edge'; +import ForkResultOkEdgeComp, { + type ForkResultOkEdge, +} from './fork-result-ok-edge'; +import { type SectionEdge, SectionOutputEdgeComp } from './section-edge'; +import SplitKeyEdgeComp, { type SplitKeyEdge } from './split-key-edge'; +import SplitRemainingEdgeComp, { + type SplitRemainingEdge, +} from './split-remaining-edge'; +import SplitSeqEdgeComp, { type SplitSeqEdge } from './split-seq-edge'; +import StreamOutEdgeComp, { type StreamOutEdge } from './stream-out-edge'; +import UnzipEdgeComp, { type UnzipEdge } from './unzip-edge'; + +export type { BufferEdge } from './buffer-edge'; +export * from './create-edge'; +export type * from './input-slots'; +export type { SectionEdge } from './section-edge'; +export type { SplitKeyEdge } from './split-key-edge'; +export type { SplitRemainingEdge } from './split-remaining-edge'; +export type { SplitSeqEdge } from './split-seq-edge'; +export type { StreamOutEdge } from './stream-out-edge'; +export type { UnzipEdge } from './unzip-edge'; + +type EdgeMapping = { + default: DefaultEdge; + unzip: UnzipEdge; + forkResultOk: ForkResultOkEdge; + forkResultErr: ForkResultErrEdge; + splitKey: SplitKeyEdge; + splitSeq: SplitSeqEdge; + splitRemaining: SplitRemainingEdge; + buffer: BufferEdge; + streamOut: StreamOutEdge; + section: SectionEdge; +}; + +export type EdgeTypes = keyof EdgeMapping; + +export type EdgeData = EdgeMapping[K]['data']; + +export type EdgeOutputData = + EdgeData['output']; + +export type EdgeInputData = + EdgeData['input']; + +export const EDGE_TYPES = { + default: DefaultEdgeComp, + unzip: UnzipEdgeComp, + forkResultOk: ForkResultOkEdgeComp, + forkResultErr: ForkResultErrEdgeComp, + splitKey: SplitKeyEdgeComp, + splitSeq: SplitSeqEdgeComp, + splitRemaining: SplitRemainingEdgeComp, + buffer: BufferEdgeComp, + streamOut: StreamOutEdgeComp, + section: SectionOutputEdgeComp, +} satisfies Record; + +export type DiagramEditorEdge = EdgeMapping[T]; + +export enum EdgeCategory { + Data, + Buffer, + Stream, +} + +export const EDGE_CATEGORIES = { + buffer: EdgeCategory.Buffer, + forkResultOk: EdgeCategory.Data, + forkResultErr: EdgeCategory.Data, + splitKey: EdgeCategory.Data, + splitSeq: EdgeCategory.Data, + splitRemaining: EdgeCategory.Data, + default: EdgeCategory.Data, + streamOut: EdgeCategory.Stream, + unzip: EdgeCategory.Data, + section: EdgeCategory.Data, +} satisfies Record; + +export type DataEdgeTypes = { + [K in EdgeTypes]: (typeof EDGE_CATEGORIES)[K] extends EdgeCategory.Data + ? K + : never; +}[EdgeTypes]; + +export function isDataEdge( + edge: DiagramEditorEdge, +): edge is DiagramEditorEdge & DataEdge, T> { + return EDGE_CATEGORIES[edge.type] === EdgeCategory.Data; +} diff --git a/diagram-editor/frontend/edges/input-slots.ts b/diagram-editor/frontend/edges/input-slots.ts new file mode 100644 index 00000000..c3547ddf --- /dev/null +++ b/diagram-editor/frontend/edges/input-slots.ts @@ -0,0 +1,21 @@ +export type DefaultInputSlotData = { type: 'default' }; + +export type SectionInputSlotData = { + type: 'sectionInput'; + inputId: string; +}; + +export type BufferKeyInputSlotData = { + type: 'bufferKey'; + key: string; +}; + +export type BufferSeqInputSlotData = { + type: 'bufferSeq'; + seq: number; +}; + +export type SectionBufferInputSlotData = { + type: 'sectionBuffer'; + inputId: string; +}; diff --git a/diagram-editor/frontend/edges/section-edge.tsx b/diagram-editor/frontend/edges/section-edge.tsx new file mode 100644 index 00000000..a674162b --- /dev/null +++ b/diagram-editor/frontend/edges/section-edge.tsx @@ -0,0 +1,17 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { DataEdge } from './data-edge'; + +export type SectionOutputData = { + output: string; +}; + +export type SectionEdge = DataEdge; + +export type SectionOutputEdgeProps = Exclude, 'label'>; + +export const SectionOutputEdgeComp = memo((props: SectionOutputEdgeProps) => { + return ( + + ); +}); diff --git a/diagram-editor/frontend/edges/split-key-edge.tsx b/diagram-editor/frontend/edges/split-key-edge.tsx new file mode 100644 index 00000000..63ec9de7 --- /dev/null +++ b/diagram-editor/frontend/edges/split-key-edge.tsx @@ -0,0 +1,17 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { DataEdge } from './data-edge'; + +export type SplitKeyOutputData = { + key: string; +}; + +export type SplitKeyEdge = DataEdge; + +export type SplitKeyEdgeProps = Exclude, 'label'>; + +function SplitKeyEdgeComp(props: SplitKeyEdgeProps) { + return ; +} + +export default memo(SplitKeyEdgeComp); diff --git a/diagram-editor/frontend/edges/split-remaining-edge.tsx b/diagram-editor/frontend/edges/split-remaining-edge.tsx new file mode 100644 index 00000000..c53ce5a5 --- /dev/null +++ b/diagram-editor/frontend/edges/split-remaining-edge.tsx @@ -0,0 +1,20 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { DataEdge } from './data-edge'; + +export type SplitRemainingOutputData = Record; +export type SplitRemainingEdge = DataEdge< + SplitRemainingOutputData, + 'splitRemaining' +>; + +export type SplitRemainingEdgeProps = Exclude< + EdgeProps, + 'label' +>; + +function SplitRemainingEdgeComp(props: SplitRemainingEdgeProps) { + return ; +} + +export default memo(SplitRemainingEdgeComp); diff --git a/diagram-editor/frontend/edges/split-seq-edge.tsx b/diagram-editor/frontend/edges/split-seq-edge.tsx new file mode 100644 index 00000000..bd2e75ee --- /dev/null +++ b/diagram-editor/frontend/edges/split-seq-edge.tsx @@ -0,0 +1,17 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { DataEdge } from './data-edge'; + +export type SplitSeqOutputData = { + seq: number; +}; + +export type SplitSeqEdge = DataEdge; + +export type SplitSeqEdgeProps = Exclude, 'label'>; + +function SplitSeqEdgeComp(props: SplitSeqEdgeProps) { + return ; +} + +export default memo(SplitSeqEdgeComp); diff --git a/diagram-editor/frontend/edges/stream-out-edge.tsx b/diagram-editor/frontend/edges/stream-out-edge.tsx new file mode 100644 index 00000000..efb76683 --- /dev/null +++ b/diagram-editor/frontend/edges/stream-out-edge.tsx @@ -0,0 +1,24 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { Edge } from '../types/react-flow'; +import type { DefaultInputSlotData } from './input-slots'; + +export type StreamOutOutputData = { + streamId: string; +}; + +export type StreamOutEdge = Edge< + StreamOutOutputData, + DefaultInputSlotData, + 'streamOut' +>; + +export type StreamOutEdgeProps = Exclude, 'label'>; + +function StreamOutEdgeComp(props: StreamOutEdgeProps) { + return ( + + ); +} + +export default memo(StreamOutEdgeComp); diff --git a/diagram-editor/frontend/edges/unzip-edge.tsx b/diagram-editor/frontend/edges/unzip-edge.tsx new file mode 100644 index 00000000..c19c31fc --- /dev/null +++ b/diagram-editor/frontend/edges/unzip-edge.tsx @@ -0,0 +1,17 @@ +import { type EdgeProps, StepEdge } from '@xyflow/react'; +import { memo } from 'react'; +import type { DataEdge } from './data-edge'; + +export type UnzipOutputData = { + seq: number; +}; + +export type UnzipEdge = DataEdge; + +export type UnzipEdgeProps = Exclude, 'label'>; + +function UnzipEdgeComp(props: UnzipEdgeProps) { + return ; +} + +export default memo(UnzipEdgeComp); diff --git a/diagram-editor/frontend/edit-templates-dialog.tsx b/diagram-editor/frontend/edit-templates-dialog.tsx new file mode 100644 index 00000000..7d59a086 --- /dev/null +++ b/diagram-editor/frontend/edit-templates-dialog.tsx @@ -0,0 +1,219 @@ +import { + Button, + ButtonGroup, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + List, + ListItem, + ListItemText, + Paper, + Stack, + TextField, + Tooltip, + useTheme, +} from '@mui/material'; +import React from 'react'; +import { EditorMode, useEditorMode } from './editor-mode'; +import { MaterialSymbol } from './nodes'; +import { useTemplates } from './templates-provider'; +import type { SectionTemplate } from './types/api'; + +export interface EditTemplatesDialogProps { + open: boolean; + onClose: () => void; +} + +interface RenamingState { + target: string; + selectAll?: boolean; + scrollTo?: boolean; +} + +function EditTemplatesDialog({ open, onClose }: EditTemplatesDialogProps) { + const theme = useTheme(); + const [templates, setTemplates] = useTemplates(); + const templateKeys = Object.keys(templates); + const [_editorMode, setEditorMode] = useEditorMode(); + const [renaming, setRenaming] = React.useState(null); + const [newId, setNewId] = React.useState(''); + const renamingRef = React.useRef(null); + + React.useEffect(() => { + if (!renamingRef.current || !renaming) { + return; + } + + renamingRef.current.focus(); + if (renaming.selectAll) { + renamingRef.current.select(); + } + if (renaming.scrollTo) { + renamingRef.current.scrollTo({ behavior: 'smooth' }); + } + }, [renaming]); + + const handleSubmitRenaming = () => { + const newTemplates: Record = {}; + // rebuild the templates in a way that keeps ordering + for (const [id, template] of Object.entries(templates)) { + if (id === renaming?.target) { + newTemplates[newId] = template; + } else { + newTemplates[id] = template; + } + } + setRenaming(null); + setNewId(''); + setTemplates(newTemplates); + }; + + return ( + + Templates + + + + {templateKeys.length > 0 ? ( + templateKeys.map((id) => ( + + + {renaming?.target === id ? ( + +
+ { + setNewId(ev.target.value); + }} + inputRef={renamingRef} + onSubmit={handleSubmitRenaming} + /> + +
+ ) : ( + {id} + )} + + {renaming?.target === id ? ( + + ) : ( + + + + )} + + + + {/* MUI has a 1px alignment error when displaying buttons of different color side by side in a ButtonGroup. + Using a different variant hides the error and also puts less focus on the "Delete" button. */} + + + + +
+
+ )) + ) : ( + + + No templates available + + + )} +
+ + + + + + +
+
+ + + +
+ ); +} + +export default EditTemplatesDialog; diff --git a/diagram-editor/frontend/editor-mode.tsx b/diagram-editor/frontend/editor-mode.tsx new file mode 100644 index 00000000..469fabe2 --- /dev/null +++ b/diagram-editor/frontend/editor-mode.tsx @@ -0,0 +1,45 @@ +import React, { createContext, type Dispatch, useContext } from 'react'; + +export enum EditorMode { + Normal, + Template, +} + +export interface EditorModeNormalContext { + mode: EditorMode.Normal; +} + +export interface EditorModeTemplateContext { + mode: EditorMode.Template; + templateId: string; +} + +export type EditorModeContext = + | EditorModeNormalContext + | EditorModeTemplateContext; + +export type UseEditorModeContext = [ + EditorModeContext, + Dispatch, +]; + +const EditorModeContextComp = createContext(null); + +export function EditorModeProvider({ + value, + children, +}: React.ProviderProps) { + return ( + + {children} + + ); +} + +export function useEditorMode(): UseEditorModeContext { + const context = useContext(EditorModeContextComp); + if (!context) { + throw new Error('useEditorMode must be used within a EditorModeProvider'); + } + return context; +} diff --git a/diagram-editor/frontend/env.d.ts b/diagram-editor/frontend/env.d.ts new file mode 100644 index 00000000..b0ac762b --- /dev/null +++ b/diagram-editor/frontend/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/diagram-editor/frontend/export-diagram-dialog.tsx b/diagram-editor/frontend/export-diagram-dialog.tsx new file mode 100644 index 00000000..c7fbfb7f --- /dev/null +++ b/diagram-editor/frontend/export-diagram-dialog.tsx @@ -0,0 +1,151 @@ +import { + Button, + ButtonGroup, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { deflateSync, strToU8 } from 'fflate'; +import React from 'react'; +import { useNodeManager } from './node-manager'; +import { MaterialSymbol } from './nodes'; +import { useTemplates } from './templates-provider'; +import { useEdges } from './use-edges'; +import { exportDiagram } from './utils/export-diagram'; + +export interface ExportDiagramDialogProps { + open: boolean; + onClose: () => void; +} + +interface DialogData { + shareLink: string; + diagramJson: string; +} + +function ExportDiagramDialog({ open, onClose }: ExportDiagramDialogProps) { + const nodeManager = useNodeManager(); + const edges = useEdges(); + const [dialogData, setDialogData] = React.useState(null); + const [templates] = useTemplates(); + + React.useLayoutEffect(() => { + if (!open) { + // To ensure that animations look correct, we need to keep the last value. + // This is also why we cannot use `useMemo` as it must return a value. + return; + } + + const diagram = exportDiagram(nodeManager, edges, templates); + const diagramJsonMin = JSON.stringify(diagram); + // Compress the JSON string to Uint8Array + const compressedData = deflateSync(strToU8(diagramJsonMin)); + // Convert Uint8Array to a binary string for btoa + let binaryString = ''; + for (let i = 0; i < compressedData.length; i++) { + binaryString += String.fromCharCode(compressedData[i]); + } + const base64Diagram = btoa(binaryString); + + const shareLink = `${window.location.origin}?diagram=${encodeURIComponent(base64Diagram)}`; + + const diagramJsonPretty = JSON.stringify(diagram, null, 2); + + const dialogData = { + shareLink, + diagramJson: diagramJsonPretty, + }; + + setDialogData(dialogData); + }, [open, nodeManager, edges, templates]); + + const handleDownload = () => { + if (!dialogData) { + return; + } + + const blob = new Blob([dialogData.diagramJson], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'diagram.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const [copiedShareLink, setCopiedShareLink] = React.useState(false); + + return ( + + Export Diagram + + + Share + + + + + + Export JSON + + + + + + + + + + ); +} + +export default ExportDiagramDialog; diff --git a/diagram-editor/frontend/forms/base-edit-operation-form.tsx b/diagram-editor/frontend/forms/base-edit-operation-form.tsx new file mode 100644 index 00000000..409387c5 --- /dev/null +++ b/diagram-editor/frontend/forms/base-edit-operation-form.tsx @@ -0,0 +1,65 @@ +import { + Card, + CardContent, + CardHeader, + IconButton, + Stack, + TextField, +} from '@mui/material'; +import type { NodeChange, NodeRemoveChange } from '@xyflow/react'; +import type React from 'react'; +import type { OperationNode, OperationNodeTypes } from '../nodes'; +import { MaterialSymbol } from '../nodes'; + +export interface BaseEditOperationFormProps< + NodeType extends OperationNodeTypes = OperationNodeTypes, +> { + node: OperationNode; + onChange?: (changes: NodeChange) => void; + onDelete?: (change: NodeRemoveChange) => void; +} + +function BaseEditOperationForm({ + node, + onChange, + onDelete, + children, +}: React.PropsWithChildren) { + return ( + + onDelete?.({ type: 'remove', id: node.id })} + > + + + } + /> + + + { + const updatedNode = { ...node }; + updatedNode.data.opId = ev.target.value; + onChange?.({ + type: 'replace', + id: node.id, + item: updatedNode, + }); + }} + /> + {children} + + + + ); +} + +export default BaseEditOperationForm; diff --git a/diagram-editor/frontend/forms/buffer-edge-form.tsx b/diagram-editor/frontend/forms/buffer-edge-form.tsx new file mode 100644 index 00000000..c6c07c4b --- /dev/null +++ b/diagram-editor/frontend/forms/buffer-edge-form.tsx @@ -0,0 +1,207 @@ +import { + Autocomplete, + FormControl, + InputLabel, + MenuItem, + Select, + TextField, +} from '@mui/material'; +import type { EdgeChange } from '@xyflow/react'; +import { useId, useMemo } from 'react'; +import type { BufferEdge } from '../edges'; +import { useNodeManager } from '../node-manager'; +import { useRegistry } from '../registry-provider'; +import { useTemplates } from '../templates-provider'; +import type { SectionTemplate } from '../types/api'; +import { exhaustiveCheck } from '../utils/exhaustive-check'; + +function getTemplateBuffers(template: SectionTemplate): string[] { + if (!template.buffers) { + return []; + } + if (Array.isArray(template.buffers)) { + return template.buffers; + } else { + return Object.keys(template.buffers); + } +} + +function defaultInputData( + inputType: BufferEdge['data']['input']['type'], +): BufferEdge['data']['input'] { + switch (inputType) { + case 'bufferKey': { + return { type: 'bufferKey', key: '' }; + } + case 'bufferSeq': { + return { type: 'bufferSeq', seq: 0 }; + } + case 'sectionBuffer': { + return { type: 'sectionBuffer', inputId: '' }; + } + default: + exhaustiveCheck(inputType); + throw new Error('unknown buffer edge input type'); + } +} + +export interface BufferEdgeInputFormProps { + edge: BufferEdge; + onChange?: (changes: EdgeChange) => void; +} + +export function BufferEdgeInputForm({ + edge, + onChange, +}: BufferEdgeInputFormProps) { + const handleDataChange = ( + event: React.ChangeEvent, + ) => { + switch (edge.data.input.type) { + case 'bufferKey': { + const newKey = event.target.value; + onChange?.({ + type: 'replace', + id: edge.id, + item: { + ...edge, + data: { ...edge.data, input: { type: 'bufferKey', key: newKey } }, + }, + }); + break; + } + case 'bufferSeq': { + const newSeq = Number.parseInt(event.target.value, 10); + if (!Number.isNaN(newSeq)) { + onChange?.({ + type: 'replace', + id: edge.id, + item: { + ...edge, + data: { ...edge.data, input: { type: 'bufferSeq', seq: newSeq } }, + }, + }); + } + break; + } + case 'sectionBuffer': { + const newBufferId = event.target.value; + onChange?.({ + type: 'replace', + id: edge.id, + item: { + ...edge, + data: { + ...edge.data, + input: { type: 'sectionBuffer', inputId: newBufferId }, + }, + }, + }); + break; + } + default: { + exhaustiveCheck(edge.data.input); + throw new Error('unknown edge input'); + } + } + }; + + const labelId = useId(); + + const nodeManager = useNodeManager(); + const targetNode = nodeManager.tryGetNode(edge.target); + const targetIsSection = targetNode?.type === 'section'; + const registry = useRegistry(); + const [templates, _setTemplates] = useTemplates(); + + const sectionBuffers = useMemo(() => { + if (!targetNode || targetNode.type !== 'section') { + return []; + } + if (typeof targetNode.data.op.builder === 'string') { + const sectionRegistration = registry.sections[targetNode.data.op.builder]; + return sectionRegistration + ? Object.keys(sectionRegistration.metadata.buffers) + : []; + } else if (typeof targetNode.data.op.template === 'string') { + const template = templates[targetNode.data.op.template]; + return template ? getTemplateBuffers(template) : []; + } else { + return []; + } + }, [targetNode, registry, templates]); + + return ( + <> + + Slot + + + {edge.data.input === undefined || + (edge.data.input.type === 'bufferSeq' && ( + + ))} + {edge.data.input.type === 'bufferKey' && ( + + )} + {edge.data.input.type === 'sectionBuffer' && ( + { + onChange?.({ + type: 'replace', + id: edge.id, + item: { + ...edge, + data: { + output: edge.data.output, + input: { type: 'sectionBuffer', inputId: value || '' }, + }, + } as BufferEdge, + }); + }} + renderInput={(params) => ( + + )} + /> + )} + + ); +} diff --git a/diagram-editor/frontend/forms/buffer-form.tsx b/diagram-editor/frontend/forms/buffer-form.tsx new file mode 100644 index 00000000..52d89ad2 --- /dev/null +++ b/diagram-editor/frontend/forms/buffer-form.tsx @@ -0,0 +1,32 @@ +import { FormControlLabel, Switch } from '@mui/material'; +import BaseEditOperationForm, { + type BaseEditOperationFormProps, +} from './base-edit-operation-form'; + +export type BufferFormProps = BaseEditOperationFormProps<'buffer'>; + +function BufferForm(props: BufferFormProps) { + return ( + + { + const updatedNode = { ...props.node }; + updatedNode.data.op.serialize = checked; + props.onChange?.({ + type: 'replace', + id: props.node.id, + item: updatedNode, + }); + }} + /> + } + label="Serialize" + /> + + ); +} + +export default BufferForm; diff --git a/diagram-editor/frontend/forms/data-input-form.tsx b/diagram-editor/frontend/forms/data-input-form.tsx new file mode 100644 index 00000000..a5d8ed54 --- /dev/null +++ b/diagram-editor/frontend/forms/data-input-form.tsx @@ -0,0 +1,85 @@ +import { Autocomplete, TextField } from '@mui/material'; +import type { EdgeChange } from '@xyflow/react'; +import { useMemo } from 'react'; +import { type DiagramEditorEdge, isDataEdge } from '../edges'; +import { useNodeManager } from '../node-manager'; +import { isSectionNode } from '../nodes'; +import { useRegistry } from '../registry-provider'; +import { useTemplates } from '../templates-provider'; +import type { SectionTemplate } from '../types/api'; + +function getTemplateInputs(template: SectionTemplate): string[] { + if (!template.inputs) { + return []; + } + if (Array.isArray(template.inputs)) { + return template.inputs; + } else { + return Object.keys(template.inputs); + } +} + +export interface DataInputEdgeFormProps { + edge: DiagramEditorEdge; + onChange?: (changes: EdgeChange) => void; +} + +export function DataInputForm({ edge, onChange }: DataInputEdgeFormProps) { + const nodeManager = useNodeManager(); + const registry = useRegistry(); + const [templates, _setTemplates] = useTemplates(); + const targetNode = nodeManager.tryGetNode(edge.target); + + const inputs = useMemo(() => { + if (!targetNode || !isSectionNode(targetNode)) { + return []; + } + + if (typeof targetNode.data.op.builder === 'string') { + const sectionBuilder = registry.sections[targetNode.data.op.builder]; + return Object.keys(sectionBuilder?.metadata.inputs || {}); + } else if (typeof targetNode.data.op.template === 'string') { + const template = templates[targetNode.data.op.template]; + return template ? getTemplateInputs(template) : []; + } else { + return []; + } + }, [targetNode, registry, templates]); + + if (!isDataEdge(edge)) { + return null; + } + + return ( + <> + {targetNode?.type === 'section' && ( + { + onChange?.({ + type: 'replace', + id: edge.id, + item: { + ...edge, + data: { + output: edge.data.output, + input: { type: 'sectionInput', inputId: value || '' }, + }, + } as DiagramEditorEdge, + }); + }} + renderInput={(params) => ( + + )} + /> + )} + + ); +} diff --git a/diagram-editor/frontend/forms/edit-edge-form.stories.tsx b/diagram-editor/frontend/forms/edit-edge-form.stories.tsx new file mode 100644 index 00000000..3180905a --- /dev/null +++ b/diagram-editor/frontend/forms/edit-edge-form.stories.tsx @@ -0,0 +1,113 @@ +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from 'storybook-react-rsbuild'; + +import EditEdgeForm from './edit-edge-form'; + +const meta: Meta = { + component: EditEdgeForm, + title: 'Forms/EditEdgeForm', +}; + +export default meta; + +type Story = StoryObj; + +const render: Story['render'] = (args) => { + const [, updateArgs] = useArgs(); + return ( + { + if (change.type === 'replace') { + updateArgs({ node: change.item }); + } + }} + /> + ); +}; + +export const BufferEdge: Story = { + args: { + edge: { + id: 'edge-1', + source: 'a', + target: 'b', + type: 'buffer', + data: { + output: {}, + input: { + type: 'bufferKey', + key: 'testKey', + }, + }, + }, + allowedEdgeTypes: ['buffer'], + }, + render, +}; + +export const ForkResult: Story = { + args: { + edge: { + id: 'edge-1', + source: 'a', + target: 'b', + type: 'forkResultOk', + data: { output: {}, input: { type: 'default' } }, + }, + allowedEdgeTypes: ['forkResultOk', 'forkResultErr'], + }, + render, +}; + +export const SplitKey: Story = { + args: { + edge: { + id: 'edge-1', + source: 'a', + target: 'b', + type: 'splitKey', + data: { + output: { + key: 'splitTestKey', + }, + input: { type: 'default' }, + }, + }, + allowedEdgeTypes: ['splitKey', 'splitSeq', 'splitRemaining'], + }, + render, +}; + +export const Unzip: Story = { + args: { + edge: { + id: 'edge-1', + source: 'a', + target: 'b', + type: 'unzip', + data: { + output: { + seq: 3, + }, + input: { type: 'default' }, + }, + }, + allowedEdgeTypes: ['unzip'], + }, + render, +}; + +export const Default: Story = { + args: { + edge: { + id: 'edge-1', + source: 'a', + target: 'b', + type: 'default', + data: { output: {}, input: { type: 'default' } }, + }, + allowedEdgeTypes: ['default'], + }, + render, +}; diff --git a/diagram-editor/frontend/forms/edit-edge-form.tsx b/diagram-editor/frontend/forms/edit-edge-form.tsx new file mode 100644 index 00000000..586f48c9 --- /dev/null +++ b/diagram-editor/frontend/forms/edit-edge-form.tsx @@ -0,0 +1,170 @@ +import { + Card, + CardContent, + CardHeader, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + Stack, +} from '@mui/material'; +import type { EdgeChange, EdgeRemoveChange } from '@xyflow/react'; +import React from 'react'; +import type { + BufferEdge, + DiagramEditorEdge, + EdgeData, + EdgeTypes, + SectionEdge, + StreamOutEdge, + UnzipEdge, +} from '../edges'; +import { MaterialSymbol } from '../nodes'; +import { exhaustiveCheck } from '../utils/exhaustive-check'; +import { BufferEdgeInputForm } from './buffer-edge-form'; +import { DataInputForm } from './data-input-form'; +import { SectionEdgeForm } from './section-form'; +import SplitEdgeForm, { type SplitEdge } from './split-edge-form'; +import UnzipEdgeForm from './unzip-edge-form'; +import { StreamOutEdgeForm } from './stream-out-edge-form'; + +const EDGE_TYPES_NAME = { + buffer: 'Buffer', + default: 'Default', + forkResultErr: 'Error', + forkResultOk: 'Ok', + splitKey: 'Key', + splitRemaining: 'Remaining', + splitSeq: 'Sequence', + streamOut: 'Stream Out', + unzip: 'Unzip', + section: 'Section', +} satisfies Record; + +const EDGE_DEFAULT_OUTPUT_DATA = { + default: { output: {}, input: { type: 'default' } }, + buffer: { output: {}, input: { type: 'bufferSeq', seq: 0 } }, + forkResultOk: { output: {}, input: { type: 'default' } }, + forkResultErr: { output: {}, input: { type: 'default' } }, + splitKey: { output: { key: '' }, input: { type: 'default' } }, + splitSeq: { output: { seq: 0 }, input: { type: 'default' } }, + splitRemaining: { output: {}, input: { type: 'default' } }, + streamOut: { + output: { streamId: '' }, + input: { type: 'default' }, + }, + unzip: { output: { seq: 0 }, input: { type: 'default' } }, + section: { + output: { output: '' }, + input: { type: 'default' }, + }, +} satisfies { [K in EdgeTypes]: EdgeData }; + +export function defaultEdgeData(type: EdgeTypes): EdgeData { + return { ...EDGE_DEFAULT_OUTPUT_DATA[type] }; +} + +export interface EditEdgeFormProps { + edge: DiagramEditorEdge; + allowedEdgeTypes: EdgeTypes[]; + onChange: (changes: EdgeChange) => void; + onDelete: (change: EdgeRemoveChange) => void; +} + +function EditEdgeForm({ + edge, + allowedEdgeTypes, + onChange, + onDelete, +}: EditEdgeFormProps) { + const subForm = React.useMemo(() => { + switch (edge.type) { + case 'buffer': { + return ( + + ); + } + case 'streamOut': { + return ( + + ); + } + case 'default': + case 'forkResultOk': + case 'forkResultErr': { + // these edges have no extra options + return null; + } + case 'splitKey': + case 'splitRemaining': + case 'splitSeq': { + return ; + } + case 'unzip': { + return ; + } + case 'section': { + return ( + + ); + } + default: { + exhaustiveCheck(edge); + throw new Error('unknown edge type'); + } + } + }, [edge, onChange]); + + const typeLabelId = React.useId(); + + return ( + + onDelete({ type: 'remove', id: edge.id })} + > + + + } + /> + + + + Type + + + + {subForm} + + + + ); +} + +export default EditEdgeForm; diff --git a/diagram-editor/frontend/forms/edit-node-form.stories.tsx b/diagram-editor/frontend/forms/edit-node-form.stories.tsx new file mode 100644 index 00000000..094c9211 --- /dev/null +++ b/diagram-editor/frontend/forms/edit-node-form.stories.tsx @@ -0,0 +1,108 @@ +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from 'storybook-react-rsbuild'; +import { ROOT_NAMESPACE } from '../utils/namespace'; +import EditNodeForm from './edit-node-form'; + +const meta: Meta = { + component: EditNodeForm, + title: 'Forms/EditNodeForm', +}; + +export default meta; + +type Story = StoryObj; + +export const Node: Story = { + args: { + node: { + id: 'node-1', + type: 'node', + position: { x: 0, y: 0 }, + data: { + namespace: ROOT_NAMESPACE, + opId: 'testOpId', + op: { + type: 'node', + builder: 'builder', + next: { builtin: 'dispose' }, + }, + }, + }, + }, + render: function Render(args) { + const [, updateArgs] = useArgs(); + return ( + { + if (change.type === 'replace') { + updateArgs({ node: change.item }); + } + }} + /> + ); + }, +}; + +export const Buffer: Story = { + args: { + node: { + id: 'buffer-1', + type: 'buffer', + position: { x: 0, y: 0 }, + data: { + namespace: ROOT_NAMESPACE, + opId: 'testOpId', + op: { + type: 'buffer', + serialize: false, + }, + }, + }, + }, + render: function Render(args) { + const [, updateArgs] = useArgs(); + return ( + { + if (change.type === 'replace') { + updateArgs({ node: change.item }); + } + }} + /> + ); + }, +}; + +export const Transform: Story = { + args: { + node: { + id: 'transform-1', + type: 'transform', + position: { x: 0, y: 0 }, + data: { + namespace: ROOT_NAMESPACE, + opId: 'testOpId', + op: { + type: 'transform', + cel: '', + next: { builtin: 'dispose' }, + }, + }, + }, + }, + render: function Render(args) { + const [, updateArgs] = useArgs(); + return ( + { + if (change.type === 'replace') { + updateArgs({ node: change.item }); + } + }} + /> + ); + }, +}; diff --git a/diagram-editor/frontend/forms/edit-node-form.tsx b/diagram-editor/frontend/forms/edit-node-form.tsx new file mode 100644 index 00000000..617ba61d --- /dev/null +++ b/diagram-editor/frontend/forms/edit-node-form.tsx @@ -0,0 +1,81 @@ +import type { NodeChange, NodeRemoveChange } from '@xyflow/react'; +import { + type DiagramEditorNode, + isOperationNode, + isSectionInterfaceNode, + type OperationNode, +} from '../nodes'; +import { exhaustiveCheck } from '../utils/exhaustive-check'; +import BaseEditOperationForm from './base-edit-operation-form'; +import BufferForm, { type BufferFormProps } from './buffer-form'; +import EditScopeForm, { type ScopeFormProps } from './edit-scope-form'; +import NodeForm, { type NodeFormProps } from './node-form'; +import { + SectionBufferForm, + SectionForm, + type SectionFormProps, + SectionInputForm, + SectionOutputForm, +} from './section-form'; +import { StreamOutForm, type StreamOutFormProps } from './stream-out-form'; +import TransformForm, { type TransformFormProps } from './transform-form'; + +interface EditOperationNodeFormProps { + node: OperationNode; + onChange?: (change: NodeChange) => void; + onDelete?: (change: NodeRemoveChange) => void; +} + +function EditOperationNodeForm(props: EditOperationNodeFormProps) { + switch (props.node.data.op.type) { + case 'node': { + return ; + } + case 'buffer': { + return ; + } + case 'scope': { + return ; + } + case 'section': { + return ; + } + case 'transform': { + return ; + } + case 'stream_out': { + return ; + } + default: { + return ; + } + } +} + +interface EditNodeFormProps { + node: DiagramEditorNode; + onChange?: (change: NodeChange) => void; + onDelete?: (change: NodeRemoveChange) => void; +} + +function EditNodeForm(props: EditNodeFormProps) { + if (isOperationNode(props.node)) { + return ; + } else if (isSectionInterfaceNode(props.node)) { + switch (props.node.type) { + case 'sectionInput': + return ; + case 'sectionOutput': + return ; + case 'sectionBuffer': + return ; + default: + exhaustiveCheck(props.node); + throw new Error('unknown node type'); + } + } else { + return null; + } +} + +export default EditNodeForm; diff --git a/diagram-editor/frontend/forms/edit-scope-form.tsx b/diagram-editor/frontend/forms/edit-scope-form.tsx new file mode 100644 index 00000000..82f709cb --- /dev/null +++ b/diagram-editor/frontend/forms/edit-scope-form.tsx @@ -0,0 +1,25 @@ +import { Button } from '@mui/material'; +import { MaterialSymbol } from '../nodes'; +import BaseEditOperationForm, { + type BaseEditOperationFormProps, +} from './base-edit-operation-form'; + +export interface ScopeFormProps extends BaseEditOperationFormProps<'scope'> { + onAddOperationClick?: React.MouseEventHandler; +} + +function EditScopeForm({ onAddOperationClick, ...otherProps }: ScopeFormProps) { + return ( + + + + ); +} + +export default EditScopeForm; diff --git a/diagram-editor/frontend/forms/index.tsx b/diagram-editor/frontend/forms/index.tsx new file mode 100644 index 00000000..8f004752 --- /dev/null +++ b/diagram-editor/frontend/forms/index.tsx @@ -0,0 +1,5 @@ +export * from './edit-edge-form'; +export { default as EditEdgeForm } from './edit-edge-form'; +export * from './edit-node-form'; +export { default as EditNodeForm } from './edit-node-form'; +export * from './edit-scope-form'; diff --git a/diagram-editor/frontend/forms/node-form.tsx b/diagram-editor/frontend/forms/node-form.tsx new file mode 100644 index 00000000..fa423841 --- /dev/null +++ b/diagram-editor/frontend/forms/node-form.tsx @@ -0,0 +1,77 @@ +import { Autocomplete, TextField } from '@mui/material'; +import { useMemo, useState } from 'react'; +import { useRegistry } from '../registry-provider'; +import BaseEditOperationForm, { + type BaseEditOperationFormProps, +} from './base-edit-operation-form'; + +export type NodeFormProps = BaseEditOperationFormProps<'node'>; + +function NodeForm(props: NodeFormProps) { + const registry = useRegistry(); + const nodes = Object.keys(registry.nodes); + const [configValue, setConfigValue] = useState(() => + props.node.data.op.config ? JSON.stringify(props.node.data.op.config) : '', + ); + const configError = useMemo(() => { + if (configValue === '') { + return false; + } + try { + JSON.parse(configValue); + return false; + } catch { + return true; + } + }, [configValue]); + + return ( + + option} + value={props.node.data.op.builder} + onChange={(_, value) => { + const updatedNode = { ...props.node }; + updatedNode.data.op.builder = value ?? ''; + props.onChange?.({ + type: 'replace', + id: props.node.id, + item: updatedNode, + }); + }} + renderInput={(params) => ( + + )} + /> + { + setConfigValue(ev.target.value); + try { + const updatedNode = { ...props.node }; + updatedNode.data.op.config = JSON.parse(ev.target.value); + props.onChange?.({ + type: 'replace', + id: props.node.id, + item: updatedNode, + }); + } catch {} + }} + error={configError} + slotProps={{ + htmlInput: { + sx: { fontFamily: 'monospace', whiteSpace: 'nowrap' }, + }, + }} + /> + + ); +} + +export default NodeForm; diff --git a/diagram-editor/frontend/forms/section-form.tsx b/diagram-editor/frontend/forms/section-form.tsx new file mode 100644 index 00000000..4dd8f76c --- /dev/null +++ b/diagram-editor/frontend/forms/section-form.tsx @@ -0,0 +1,257 @@ +import { + Autocomplete, + Card, + CardContent, + CardHeader, + IconButton, + Stack, + TextField, +} from '@mui/material'; +import type { EdgeChange, NodeChange, NodeRemoveChange } from '@xyflow/react'; +import { type PropsWithChildren, useMemo } from 'react'; +import type { SectionEdge } from '../edges'; +import { useNodeManager } from '../node-manager'; +import { + isSectionNode, + MaterialSymbol, + type SectionBufferNode, + type SectionInputNode, + type SectionInterfaceNode, + type SectionOutputNode, +} from '../nodes'; +import { useRegistry } from '../registry-provider'; +import { useTemplates } from '../templates-provider'; +import BaseEditOperationForm, { + type BaseEditOperationFormProps, +} from './base-edit-operation-form'; + +export interface BaseSectionInterfaceFormProps { + node: SectionInterfaceNode; + onDelete?: (change: NodeRemoveChange) => void; +} + +function BaseSectionInterfaceForm({ + node, + onDelete, + children, +}: PropsWithChildren) { + return ( + + onDelete?.({ type: 'remove', id: node.id })} + > + + + } + /> + {children} + + ); +} + +export interface SectionInputFormProps extends BaseSectionInterfaceFormProps { + node: SectionInputNode; + onChange?: (change: NodeChange) => void; +} + +export function SectionInputForm({ + node, + onChange, + onDelete, +}: SectionInputFormProps) { + return ( + + + { + const updatedNode = { ...node }; + updatedNode.data.remappedId = ev.target.value; + onChange?.({ + type: 'replace', + id: node.id, + item: updatedNode, + }); + }} + /> + + + ); +} + +export interface SectionBufferFormProps extends BaseSectionInterfaceFormProps { + node: SectionBufferNode; + onChange?: (change: NodeChange) => void; +} + +export function SectionBufferForm({ + node, + onChange, + onDelete, +}: SectionBufferFormProps) { + return ( + + + { + const updatedNode = { ...node }; + updatedNode.data.remappedId = ev.target.value; + onChange?.({ + type: 'replace', + id: node.id, + item: updatedNode, + }); + }} + /> + + + ); +} + +export interface SectionOutputFormProps extends BaseSectionInterfaceFormProps { + node: SectionOutputNode; + onChange?: (change: NodeChange) => void; +} + +export function SectionOutputForm({ + node, + onChange, + onDelete, +}: SectionOutputFormProps) { + return ( + + + { + const updatedNode = { ...node }; + updatedNode.data.outputId = ev.target.value; + onChange?.({ + type: 'replace', + id: node.id, + item: updatedNode, + }); + }} + /> + + + ); +} + +export type SectionFormProps = BaseEditOperationFormProps<'section'>; + +export function SectionForm(props: SectionFormProps) { + const registry = useRegistry(); + const [templates, _setTemplates] = useTemplates(); + const sectionBuilders = Object.keys(registry.sections); + const sectionTemplates = Object.keys(templates); + const sections = [...sectionBuilders, ...sectionTemplates].sort(); + + return ( + + option} + value={ + (props.node.data.op.builder || + props.node.data.op.template || + '') as string + } + onChange={(_, value) => { + if (value === null) { + return; + } + + const updatedNode = { ...props.node }; + if (sectionBuilders.includes(value)) { + updatedNode.data.op.builder = value; + delete updatedNode.data.op.template; + } else if (sectionTemplates.includes(value)) { + updatedNode.data.op.template = value; + delete updatedNode.data.op.builder; + } else { + // unable to determine if selected option is a builder or template, assume template. + updatedNode.data.op.template = value; + delete updatedNode.data.op.builder; + } + props.onChange?.({ + type: 'replace', + id: props.node.id, + item: updatedNode, + }); + }} + renderInput={(params) => ( + + )} + /> + + ); +} + +export interface SectionEdgeFormProps { + edge: SectionEdge; + onChange?: (changes: EdgeChange) => void; +} + +export function SectionEdgeForm({ edge, onChange }: SectionEdgeFormProps) { + const nodeManager = useNodeManager(); + const registry = useRegistry(); + const [templates, _setTemplates] = useTemplates(); + const sourceNode = nodeManager.tryGetNode(edge.source); + + const outputs = useMemo(() => { + if (sourceNode && isSectionNode(sourceNode)) { + if (typeof sourceNode.data.op.builder === 'string') { + const sectionBuilder = registry.sections[sourceNode.data.op.builder]; + return Object.keys(sectionBuilder?.metadata.outputs || {}); + } else if (typeof sourceNode.data.op.template === 'string') { + const template = templates[sourceNode.data.op.template]; + return template?.outputs || []; + } else { + return []; + } + } else { + console.error('source node of a section edge is not a section node'); + return []; + } + }, [sourceNode, registry, templates]); + + return ( + option} + value={edge.data.output.output} + onChange={(_, value) => { + onChange?.({ + type: 'replace', + id: edge.id, + item: { + ...edge, + data: { + ...edge.data, + output: { output: value || '' }, + }, + }, + }); + }} + renderInput={(params) => ( + + )} + /> + ); +} diff --git a/diagram-editor/frontend/forms/split-edge-form.tsx b/diagram-editor/frontend/forms/split-edge-form.tsx new file mode 100644 index 00000000..2ab1bcde --- /dev/null +++ b/diagram-editor/frontend/forms/split-edge-form.tsx @@ -0,0 +1,58 @@ +import { TextField } from '@mui/material'; +import type { EdgeChange } from '@xyflow/react'; +import type { SplitKeyEdge, SplitRemainingEdge, SplitSeqEdge } from '../edges'; + +export type SplitEdge = SplitKeyEdge | SplitSeqEdge | SplitRemainingEdge; + +export interface SplitEdgeFormProps { + edge: SplitEdge; + onChange?: (change: EdgeChange) => void; +} + +function SplitEdgeForm({ edge, onChange }: SplitEdgeFormProps) { + const handleDataChange = ( + event: React.ChangeEvent, + ) => { + if (edge.type === 'splitKey') { + const newKey = event.target.value; + onChange?.({ + type: 'replace', + id: edge.id, + item: { ...edge, data: { ...edge.data, output: { key: newKey } } }, + }); + } else if (edge.type === 'splitSeq') { + const newSeq = Number.parseInt(event.target.value, 10); + if (!Number.isNaN(newSeq)) { + onChange?.({ + type: 'replace', + id: edge.id, + item: { ...edge, data: { ...edge.data, output: { seq: newSeq } } }, + }); + } + } + }; + + return ( + <> + {edge.type === 'splitKey' && ( + + )} + {edge.type === 'splitSeq' && ( + + )} + + ); +} + +export default SplitEdgeForm; diff --git a/diagram-editor/frontend/forms/stream-out-edge-form.tsx b/diagram-editor/frontend/forms/stream-out-edge-form.tsx new file mode 100644 index 00000000..f20006c3 --- /dev/null +++ b/diagram-editor/frontend/forms/stream-out-edge-form.tsx @@ -0,0 +1,35 @@ +import { TextField } from '@mui/material'; +import type { EdgeChange } from '@xyflow/react'; +import type { StreamOutEdge } from '../edges'; + +export interface StreamOutEdgeFormProps { + edge: StreamOutEdge; + onChange?: (change: EdgeChange) => void; +} + +export function StreamOutEdgeForm({ edge, onChange }: StreamOutEdgeFormProps) { + const handleDataChange = ( + event: React.ChangeEvent, + ) => { + const newStreamId = event.target.value; + onChange?.({ + type: 'replace', + id: edge.id, + item: { + ...edge, + data: { ...edge.data, output: { streamId: newStreamId } }, + }, + }); + }; + + return ( + <> + + + ); +} diff --git a/diagram-editor/frontend/forms/stream-out-form.tsx b/diagram-editor/frontend/forms/stream-out-form.tsx new file mode 100644 index 00000000..bb7f899f --- /dev/null +++ b/diagram-editor/frontend/forms/stream-out-form.tsx @@ -0,0 +1,28 @@ +import { TextField } from '@mui/material'; +import BaseEditOperationForm, { + type BaseEditOperationFormProps, +} from './base-edit-operation-form'; + +export type StreamOutFormProps = BaseEditOperationFormProps<'stream_out'>; + +export function StreamOutForm(props: StreamOutFormProps) { + return ( + + { + const updatedNode = { ...props.node }; + updatedNode.data.op.name = ev.target.value; + props.onChange?.({ + type: 'replace', + id: props.node.id, + item: updatedNode, + }); + }} + /> + + ); +} diff --git a/diagram-editor/frontend/forms/transform-form.stories.tsx b/diagram-editor/frontend/forms/transform-form.stories.tsx new file mode 100644 index 00000000..cf9dcbf3 --- /dev/null +++ b/diagram-editor/frontend/forms/transform-form.stories.tsx @@ -0,0 +1,47 @@ +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from 'storybook-react-rsbuild'; +import { ROOT_NAMESPACE } from '../utils/namespace'; +import TransformForm from './transform-form'; + +const meta: Meta = { + component: TransformForm, + title: 'Forms/TransformForm', +}; + +export default meta; + +type Story = StoryObj; + +const render: Story['render'] = (args) => { + const [, updateArgs] = useArgs(); + return ( + { + if (change.type === 'replace') { + updateArgs({ node: change.item }); + } + }} + /> + ); +}; + +export const Default: Story = { + args: { + node: { + id: 'transform-1', + type: 'transform', + position: { x: 0, y: 0 }, + data: { + namespace: ROOT_NAMESPACE, + opId: 'testOpId', + op: { + type: 'transform', + cel: 'request + 1', + next: { builtin: 'dispose' }, + }, + }, + }, + }, + render, +}; diff --git a/diagram-editor/frontend/forms/transform-form.tsx b/diagram-editor/frontend/forms/transform-form.tsx new file mode 100644 index 00000000..49af6946 --- /dev/null +++ b/diagram-editor/frontend/forms/transform-form.tsx @@ -0,0 +1,54 @@ +import { TextField } from '@mui/material'; +import { parse } from 'cel-js'; +import React from 'react'; +import BaseEditOperationForm, { + type BaseEditOperationFormProps, +} from './base-edit-operation-form'; + +export type TransformFormProps = BaseEditOperationFormProps<'transform'>; + +function TransformForm(props: TransformFormProps) { + const celError = React.useMemo( + () => parse(props.node.data.op.cel).isSuccess, + [props.node.data.op.cel], + ); + return ( + + { + const updatedNode = { + ...props.node, + data: { + ...props.node.data, + op: { + ...props.node.data.op, + cel: ev.target.value, + }, + }, + }; + props.onChange?.({ + type: 'replace', + id: props.node.id, + item: updatedNode, + }); + }} + /> + + ); +} + +export default TransformForm; diff --git a/diagram-editor/frontend/forms/unzip-edge-form.tsx b/diagram-editor/frontend/forms/unzip-edge-form.tsx new file mode 100644 index 00000000..6457a5b8 --- /dev/null +++ b/diagram-editor/frontend/forms/unzip-edge-form.tsx @@ -0,0 +1,33 @@ +import { TextField } from '@mui/material'; +import type { EdgeChange } from '@xyflow/react'; +import type { UnzipEdge } from '../edges'; + +export interface UnzipEdgeFormProps { + edge: UnzipEdge; + onChange?: (change: EdgeChange) => void; +} + +function UnzipEdgeForm({ edge, onChange }: UnzipEdgeFormProps) { + return ( + <> + { + const value = Number.parseInt(ev.target.value, 10); + if (!Number.isNaN(value) && edge.data) { + edge.data.seq = value; + onChange?.({ + type: 'replace', + id: edge.id, + item: edge, + }); + } + }} + /> + + ); +} + +export default UnzipEdgeForm; diff --git a/diagram-editor/frontend/handles.tsx b/diagram-editor/frontend/handles.tsx new file mode 100644 index 00000000..0c25ed1d --- /dev/null +++ b/diagram-editor/frontend/handles.tsx @@ -0,0 +1,64 @@ +import { + Handle as ReactFlowHandle, + type HandleProps as ReactFlowHandleProps, +} from '@xyflow/react'; +import { exhaustiveCheck } from './utils/exhaustive-check'; + +export type HandleId = 'stream' | null | undefined; + +export enum HandleType { + Data, + Buffer, + Stream, + DataBuffer, + DataStream, +} + +export interface HandleProps extends Omit { + variant: HandleType; +} + +function variantClassName(handleType?: HandleType): string | undefined { + if (handleType === undefined) { + return undefined; + } + + switch (handleType) { + case HandleType.Data: { + // use the default style + return undefined; + } + case HandleType.Buffer: { + return 'handle-buffer'; + } + case HandleType.Stream: { + return 'handle-stream'; + } + case HandleType.DataBuffer: { + return 'handle-data-buffer'; + } + case HandleType.DataStream: { + return 'handle-data-stream'; + } + default: { + exhaustiveCheck(handleType); + throw new Error('unknown edge category'); + } + } +} + +export function Handle({ variant, className, ...baseProps }: HandleProps) { + const handleId = variant === HandleType.Stream ? 'stream' : undefined; + + const prependClassName = className + ? `${variantClassName(variant)} ${className} ` + : variantClassName(variant); + + return ( + + ); +} diff --git a/diagram-editor/frontend/index.tsx b/diagram-editor/frontend/index.tsx new file mode 100644 index 00000000..bfc95fc3 --- /dev/null +++ b/diagram-editor/frontend/index.tsx @@ -0,0 +1,14 @@ +import '@material-symbols/font-400'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './app'; + +const rootEl = document.getElementById('root'); +if (rootEl) { + const root = ReactDOM.createRoot(rootEl); + root.render( + + + , + ); +} diff --git a/diagram-editor/frontend/node-manager.ts b/diagram-editor/frontend/node-manager.ts new file mode 100644 index 00000000..bd32abb4 --- /dev/null +++ b/diagram-editor/frontend/node-manager.ts @@ -0,0 +1,154 @@ +import { createContext, useContext } from 'react'; +import type { DiagramEditorEdge } from './edges'; +import { + type DiagramEditorNode, + isBuiltinNode, + isOperationNode, + TERMINATE_ID, +} from './nodes'; +import type { NextOperation } from './types/api'; +import { exhaustiveCheck } from './utils/exhaustive-check'; +import { joinNamespaces, ROOT_NAMESPACE } from './utils/namespace'; +import { isBuiltin } from './utils/operation'; + +export class NodeManager { + private nodeIdMap: Map = new Map(); + private namespacedOpIdMap: Map = new Map(); + + constructor(public nodes: DiagramEditorNode[]) { + for (const node of nodes) { + this.nodeIdMap.set(node.id, node); + if (isOperationNode(node)) { + const namespacedOpId = joinNamespaces( + node.data.namespace, + node.data.opId, + ); + this.namespacedOpIdMap.set(namespacedOpId, node); + } else if (isBuiltinNode(node)) { + this.namespacedOpIdMap.set(node.id, node); + } else if (node.type === 'sectionOutput') { + // This will conflict if there is an op in a template with the same id as an output. + // However, this conflict exist in bevy_impulse as well, so we assume that it will not happen. + this.namespacedOpIdMap.set( + joinNamespaces(ROOT_NAMESPACE, node.data.outputId), + node, + ); + } + } + } + + tryGetNode(nodeId: string): DiagramEditorNode | null { + return this.nodeIdMap.get(nodeId) || null; + } + + getNode(nodeId: string): DiagramEditorNode { + const node = this.tryGetNode(nodeId); + if (!node) { + throw new Error(`cannot find node "${nodeId}"`); + } + return node; + } + + iterNodes(): MapIterator { + return this.nodeIdMap.values(); + } + + hasNode(nodeId: string): boolean { + try { + this.getNode(nodeId); + return true; + } catch { + return false; + } + } + + getNodeFromNamespaceOpId(namespace: string, opId: string): DiagramEditorNode { + const namespacedOpId = joinNamespaces(namespace, opId); + const node = this.namespacedOpIdMap.get(namespacedOpId); + if (!node) { + throw new Error(`cannot find node for operation "${namespacedOpId}"`); + } + return node; + } + + getNodeFromRootOpId(opId: string): DiagramEditorNode { + return this.getNodeFromNamespaceOpId(ROOT_NAMESPACE, opId); + } + + /** + * @returns returns `null` if the next operation points to a builtin node not rendered in the editor (e.g. `dispose`, `cancel`). + */ + getNodeFromNextOp( + namespace: string, + nextOp: NextOperation, + ): DiagramEditorNode | null { + if (isBuiltin(nextOp)) { + switch (nextOp.builtin) { + case 'dispose': + case 'cancel': + return null; + case 'terminate': + return this.getNode(joinNamespaces(namespace, TERMINATE_ID)); + } + } + + const opId = (() => { + if (typeof nextOp === 'object') { + return Object.keys(nextOp)[0]; + } + return nextOp; + })(); + + const namespacedOpId = joinNamespaces(namespace, opId); + const node = this.namespacedOpIdMap.get(namespacedOpId); + if (!node) { + throw new Error( + `cannot find operation ${joinNamespaces(namespace, opId)}`, + ); + } + return node; + } + + getTargetNextOp(edge: DiagramEditorEdge): NextOperation { + // TODO: Validate that the edge does not traverse namespaces + switch (edge.data.input.type) { + case 'bufferKey': + case 'bufferSeq': + case 'default': { + const target = this.getNode(edge.target); + if (isBuiltinNode(target)) { + return { builtin: target.type }; + } + if (isOperationNode(target)) { + return target.data.opId; + } + if (target.type === 'sectionOutput') { + return target.data.outputId; + } + throw new Error('unknown node type'); + } + case 'sectionBuffer': + case 'sectionInput': { + const target = this.getNode(edge.target); + if (target.type !== 'section') { + throw new Error( + 'edge is connecting to a section input slot but target is not a section', + ); + } + return { [target.data.opId]: edge.data.input.inputId }; + } + default: { + exhaustiveCheck(edge.data.input); + throw new Error('unknown edge input'); + } + } + } +} + +const NodeManagerContext = createContext(new NodeManager([])); + +export const NodeManagerProvider = NodeManagerContext.Provider; + +export function useNodeManager() { + return useContext(NodeManagerContext); +} diff --git a/diagram-editor/frontend/nodes/base-node.tsx b/diagram-editor/frontend/nodes/base-node.tsx new file mode 100644 index 00000000..dffe268d --- /dev/null +++ b/diagram-editor/frontend/nodes/base-node.tsx @@ -0,0 +1,117 @@ +import { + Box, + Button, + type ButtonProps, + Paper, + Stack, + Typography, +} from '@mui/material'; +import { type NodeProps, Position } from '@xyflow/react'; +import { memo } from 'react'; +import { Handle, type HandleProps, HandleType } from '../handles'; +import { LAYOUT_OPTIONS } from '../utils/layout'; + +export interface BaseNodeProps extends NodeProps { + color?: ButtonProps['color']; + icon?: React.JSX.Element | string; + label: string; + variant: 'input' | 'output' | 'inputOutput'; + /** + * defaults to `HandleType.Data`. + */ + inputHandleType?: HandleType; + /** + * defaults to `HandleType.Data`. + */ + outputHandleType?: HandleType; + caption?: string; + extraHandles?: HandleProps[]; +} + +function BaseNode({ + color, + icon: materialIconOrSymbol, + label, + variant, + inputHandleType = HandleType.Data, + outputHandleType = HandleType.Data, + caption, + extraHandles, + isConnectable, + selected, + sourcePosition = Position.Bottom, + targetPosition = Position.Top, +}: BaseNodeProps) { + const icon = + typeof materialIconOrSymbol === 'string' ? ( + + ) : ( + materialIconOrSymbol + ); + + return ( + + {(variant === 'input' || variant === 'inputOutput') && ( + + )} + + {(variant === 'output' || variant === 'inputOutput') && ( + + )} + {extraHandles?.map((handleProps, i) => ( + + ))} + + ); +} + +export default memo(BaseNode); diff --git a/diagram-editor/frontend/nodes/buffer-access-node.tsx b/diagram-editor/frontend/nodes/buffer-access-node.tsx new file mode 100644 index 00000000..517272fe --- /dev/null +++ b/diagram-editor/frontend/nodes/buffer-access-node.tsx @@ -0,0 +1,21 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { HandleType } from '../handles'; +import { BufferAccessIcon } from './icons'; + +function BufferAccessNodeComp( + props: NodeProps>, +) { + return ( + } + label="Buffer Access" + variant="inputOutput" + inputHandleType={HandleType.DataBuffer} + /> + ); +} + +export default BufferAccessNodeComp; diff --git a/diagram-editor/frontend/nodes/buffer-node.tsx b/diagram-editor/frontend/nodes/buffer-node.tsx new file mode 100644 index 00000000..5641bd2c --- /dev/null +++ b/diagram-editor/frontend/nodes/buffer-node.tsx @@ -0,0 +1,19 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { HandleType } from '../handles'; +import { BufferIcon } from './icons'; + +function BufferNodeComp(props: NodeProps>) { + return ( + } + label="Buffer" + variant="inputOutput" + outputHandleType={HandleType.Buffer} + /> + ); +} + +export default BufferNodeComp; diff --git a/diagram-editor/frontend/nodes/create-node.ts b/diagram-editor/frontend/nodes/create-node.ts new file mode 100644 index 00000000..cfb36c6e --- /dev/null +++ b/diagram-editor/frontend/nodes/create-node.ts @@ -0,0 +1,167 @@ +import type { XYPosition } from '@xyflow/react'; +import { v4 as uuidv4 } from 'uuid'; +import type { DiagramOperation, NextOperation } from '../types/api'; +import { calculateScopeBounds, LAYOUT_OPTIONS } from '../utils/layout'; +import { joinNamespaces } from '../utils/namespace'; +import { + type DiagramEditorNode, + type OperationNode, + type SectionBufferNode, + type SectionInputNode, + type SectionOutputNode, + START_ID, + TERMINATE_ID, +} from '.'; + +export function createStartNode( + namespace: string, + position: XYPosition, +): DiagramEditorNode { + return { + // NOTE: atm `NodeManager` relies on how the id of builtin nodes is computed to locate them. + id: joinNamespaces(namespace, START_ID), + type: 'start', + position, + selectable: false, + data: { namespace }, + }; +} + +export function createTerminateNode( + namespace: string, + position: XYPosition, +): DiagramEditorNode { + return { + // NOTE: atm `NodeManager` relies on how the id of builtin nodes is computed to locate them. + id: joinNamespaces(namespace, TERMINATE_ID), + type: 'terminate', + position, + selectable: false, + data: { namespace }, + }; +} + +/** + * Create a section input node. For simplicity, remapping the input is not supported, `targetId` + * must point to the id of an operation in the template. + */ +export function createSectionInputNode( + remappedId: string, + targetId: NextOperation, + position: XYPosition, +): SectionInputNode { + return { + id: uuidv4(), + type: 'sectionInput', + data: { remappedId, targetId }, + position, + }; +} + +export function createSectionOutputNode( + outputId: string, + position: XYPosition, +): SectionOutputNode { + return { + id: uuidv4(), + type: 'sectionOutput', + data: { outputId }, + position, + }; +} + +/** + * Create a section input node. For simplicity, remapping the input is not supported, `targetId` + * must point to the id of a `buffer` operation in the template. + */ +export function createSectionBufferNode( + remappedId: string, + targetId: NextOperation, + position: XYPosition, +): SectionBufferNode { + return { + id: uuidv4(), + type: 'sectionBuffer', + data: { remappedId, targetId }, + position, + }; +} + +export function createOperationNode( + namespace: string, + parentId: string | undefined, + position: XYPosition, + op: Exclude, + opId: string, +): OperationNode { + return { + id: uuidv4(), + type: op.type, + position, + data: { + namespace, + opId, + op, + }, + ...(parentId && { parentId }), + }; +} + +export function createScopeNode( + namespace: string, + parentId: string | undefined, + position: XYPosition, + op: DiagramOperation & { type: 'scope' }, + opId: string, +): [OperationNode<'scope'>, ...DiagramEditorNode[]] { + const scopeId = uuidv4(); + const children: DiagramEditorNode[] = [ + { + id: joinNamespaces(namespace, opId, START_ID), + type: 'start', + position: { + x: LAYOUT_OPTIONS.scopePadding.leftRight, + y: LAYOUT_OPTIONS.scopePadding.topBottom, + }, + data: { + namespace: joinNamespaces(namespace, scopeId), + }, + parentId: scopeId, + }, + { + id: joinNamespaces(namespace, opId, TERMINATE_ID), + type: 'terminate', + position: { + x: LAYOUT_OPTIONS.scopePadding.leftRight, + y: LAYOUT_OPTIONS.scopePadding.topBottom * 5, + }, + data: { + namespace: joinNamespaces(namespace, scopeId), + }, + parentId: scopeId, + }, + ]; + const scopeBounds = calculateScopeBounds( + children.map((child) => child.position), + ); + return [ + { + id: scopeId, + type: 'scope', + position: { + x: position.x + scopeBounds.x, + y: position.y + scopeBounds.y, + }, + data: { + namespace, + opId, + op, + }, + width: scopeBounds.width, + height: scopeBounds.height, + zIndex: -1, + ...(parentId && { parentId }), + }, + ...children, + ]; +} diff --git a/diagram-editor/frontend/nodes/fork-clone-node.tsx b/diagram-editor/frontend/nodes/fork-clone-node.tsx new file mode 100644 index 00000000..477d195a --- /dev/null +++ b/diagram-editor/frontend/nodes/fork-clone-node.tsx @@ -0,0 +1,17 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { ForkCloneIcon } from './icons'; + +function ForkCloneNodeComp(props: NodeProps>) { + return ( + } + label="Fork Clone" + variant="inputOutput" + /> + ); +} + +export default ForkCloneNodeComp; diff --git a/diagram-editor/frontend/nodes/fork-result-node.tsx b/diagram-editor/frontend/nodes/fork-result-node.tsx new file mode 100644 index 00000000..2df1ec07 --- /dev/null +++ b/diagram-editor/frontend/nodes/fork-result-node.tsx @@ -0,0 +1,17 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { ForkResultIcon } from './icons'; + +function ForkResultNodeComp(props: NodeProps>) { + return ( + } + label="Fork Result" + variant="inputOutput" + /> + ); +} + +export default ForkResultNodeComp; diff --git a/diagram-editor/frontend/nodes/icons.tsx b/diagram-editor/frontend/nodes/icons.tsx new file mode 100644 index 00000000..d10f55cd --- /dev/null +++ b/diagram-editor/frontend/nodes/icons.tsx @@ -0,0 +1,123 @@ +import { Box, type BoxProps } from '@mui/material'; +import type React from 'react'; +import type { DiagramOperation } from '../types/api'; +import { exhaustiveCheck } from '../utils/exhaustive-check'; + +export interface MaterialSymbolProps extends BoxProps { + symbol: string; +} + +export function MaterialSymbol({ + symbol, + ...otherProps +}: MaterialSymbolProps): React.JSX.Element { + return ( + + {symbol} + + ); +} + +export function NodeIcon(): React.JSX.Element { + return ; +} + +export function ForkCloneIcon(): React.JSX.Element { + return ; +} + +export function TransformIcon(): React.JSX.Element { + return ; +} + +export function BufferIcon(): React.JSX.Element { + return ; +} + +export function BufferAccessIcon(): React.JSX.Element { + return ; +} + +export function SplitIcon(): React.JSX.Element { + return ( + + ); +} + +export function ForkResultIcon(): React.JSX.Element { + return ; +} + +export function ListenIcon(): React.JSX.Element { + return ; +} + +export function JoinIcon(): React.JSX.Element { + return ; +} + +export const SerializedJoinIcon = JoinIcon; + +export function StreamOutIcon(): React.JSX.Element { + return ; +} + +export function ScopeIcon(): React.JSX.Element { + return ; +} + +export function SectionIcon(): React.JSX.Element { + return ; +} + +export function SectionInputIcon(): React.JSX.Element { + return ; +} + +export function SectionOutputIcon(): React.JSX.Element { + return ; +} + +export function SectionBufferIcon(): React.JSX.Element { + return ; +} + +export function UnzipIcon(): React.JSX.Element { + return ; +} + +export function getIcon(op: DiagramOperation): React.ComponentType { + switch (op.type) { + case 'node': + return NodeIcon; + case 'section': + return SectionIcon; + case 'fork_clone': + return ForkCloneIcon; + case 'unzip': + return UnzipIcon; + case 'fork_result': + return ForkResultIcon; + case 'split': + return SplitIcon; + case 'join': + return JoinIcon; + case 'serialized_join': + return SerializedJoinIcon; + case 'transform': + return TransformIcon; + case 'buffer': + return BufferIcon; + case 'buffer_access': + return BufferAccessIcon; + case 'listen': + return ListenIcon; + case 'scope': + return ScopeIcon; + case 'stream_out': + return StreamOutIcon; + default: + exhaustiveCheck(op); + throw new Error('unknown op'); + } +} diff --git a/diagram-editor/frontend/nodes/index.ts b/diagram-editor/frontend/nodes/index.ts new file mode 100644 index 00000000..da734e3d --- /dev/null +++ b/diagram-editor/frontend/nodes/index.ts @@ -0,0 +1,88 @@ +import type { DiagramOperation } from '../types/api'; +import type { Node } from '../types/react-flow'; +import BufferAccessNodeComp from './buffer-access-node'; +import BufferNodeComp from './buffer-node'; +import ForkCloneNodeComp from './fork-clone-node'; +import ForkResultNodeComp from './fork-result-node'; +import JoinNodeComp from './join-node'; +import ListenNodeComp from './listen-node'; +import NodeNodeComp from './node-node'; +import ScopeNodeComp from './scope-node'; +import SectionNodeComp, { + SectionBufferNodeComp, + SectionInputNodeComp, + type SectionInterfaceNode, + SectionOutputNodeComp, +} from './section-node'; +import SerializedJoinNodeComp from './serialized-join-node'; +import SplitNodeComp from './split-node'; +import StartNodeComp from './start-node'; +import StreamOutNodeComp from './stream-out-node'; +import TerminateNodeComp from './terminate-node'; +import TransformNodeComp from './transform-node'; +import UnzipNodeComp from './unzip-node'; + +export * from './create-node'; +export * from './icons'; +export type { + SectionBufferData, + SectionBufferNode, + SectionInputData, + SectionInputNode, + SectionInterfaceNode, + SectionInterfaceNodeTypes, + SectionOutputData, + SectionOutputNode, +} from './section-node'; +export * from './utils'; + +export const START_ID = '__builtin_start__'; +export const TERMINATE_ID = '__builtin_terminate__'; + +export const NODE_TYPES = { + start: StartNodeComp, + terminate: TerminateNodeComp, + node: NodeNodeComp, + section: SectionNodeComp, + sectionInput: SectionInputNodeComp, + sectionOutput: SectionOutputNodeComp, + sectionBuffer: SectionBufferNodeComp, + fork_clone: ForkCloneNodeComp, + unzip: UnzipNodeComp, + fork_result: ForkResultNodeComp, + split: SplitNodeComp, + join: JoinNodeComp, + serialized_join: SerializedJoinNodeComp, + transform: TransformNodeComp, + buffer: BufferNodeComp, + buffer_access: BufferAccessNodeComp, + listen: ListenNodeComp, + scope: ScopeNodeComp, + stream_out: StreamOutNodeComp, +}; + +export type NodeTypes = keyof typeof NODE_TYPES; + +export type BuiltinNodeData = { namespace: string }; + +export type BuiltinNodeTypes = 'start' | 'terminate'; + +export type BuiltinNode = Node & { + type: BuiltinNodeTypes; +}; + +export type OperationNodeData = { + namespace: string; + opId: string; + op: DiagramOperation & { type: K }; +}; + +export type OperationNodeTypes = DiagramOperation['type']; + +export type OperationNode = + Node, K>; + +export type DiagramEditorNode = + | BuiltinNode + | OperationNode + | SectionInterfaceNode; diff --git a/diagram-editor/frontend/nodes/join-node.tsx b/diagram-editor/frontend/nodes/join-node.tsx new file mode 100644 index 00000000..e8d07966 --- /dev/null +++ b/diagram-editor/frontend/nodes/join-node.tsx @@ -0,0 +1,19 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { HandleType } from '../handles'; +import { JoinIcon } from './icons'; + +function JoinNodeComp(props: NodeProps>) { + return ( + } + label="Join" + variant="inputOutput" + inputHandleType={HandleType.Buffer} + /> + ); +} + +export default JoinNodeComp; diff --git a/diagram-editor/frontend/nodes/listen-node.tsx b/diagram-editor/frontend/nodes/listen-node.tsx new file mode 100644 index 00000000..140a76fb --- /dev/null +++ b/diagram-editor/frontend/nodes/listen-node.tsx @@ -0,0 +1,19 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { HandleType } from '../handles'; +import { ListenIcon } from './icons'; + +function ListenNodeComp(props: NodeProps>) { + return ( + } + label="Listen" + variant="inputOutput" + inputHandleType={HandleType.Buffer} + /> + ); +} + +export default ListenNodeComp; diff --git a/diagram-editor/frontend/nodes/node-node.tsx b/diagram-editor/frontend/nodes/node-node.tsx new file mode 100644 index 00000000..f245a345 --- /dev/null +++ b/diagram-editor/frontend/nodes/node-node.tsx @@ -0,0 +1,26 @@ +import { type NodeProps, Position } from '@xyflow/react'; +import { HandleType } from '../handles'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { NodeIcon } from './icons'; + +function NodeNodeComp(props: NodeProps>) { + return ( + } + label={props.data.op.builder || 'Select Builder'} + variant="inputOutput" + outputHandleType={HandleType.Data} + extraHandles={[ + { + position: Position.Right, + type: 'source', + variant: HandleType.Stream, + }, + ]} + /> + ); +} + +export default NodeNodeComp; diff --git a/diagram-editor/frontend/nodes/scope-node.tsx b/diagram-editor/frontend/nodes/scope-node.tsx new file mode 100644 index 00000000..0c045317 --- /dev/null +++ b/diagram-editor/frontend/nodes/scope-node.tsx @@ -0,0 +1,48 @@ +import { Box, useTheme } from '@mui/material'; +import { type NodeProps, Position } from '@xyflow/react'; +import { Handle, HandleType } from '../handles'; +import type { OperationNode } from '.'; + +function ScopeNodeComp({ + isConnectable, + sourcePosition = Position.Bottom, + targetPosition = Position.Top, + width, + height, +}: NodeProps>) { + const theme = useTheme(); + + return ( + <> + + + + + + ); +} + +export default ScopeNodeComp; diff --git a/diagram-editor/frontend/nodes/section-node.tsx b/diagram-editor/frontend/nodes/section-node.tsx new file mode 100644 index 00000000..36e21caf --- /dev/null +++ b/diagram-editor/frontend/nodes/section-node.tsx @@ -0,0 +1,97 @@ +import type { NodeProps } from '@xyflow/react'; +import { HandleType } from '../handles'; +import type { NextOperation } from '../types/api'; +import type { Node } from '../types/react-flow'; +import { isSectionBuilder } from '../utils/operation'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { + SectionBufferIcon, + SectionIcon, + SectionInputIcon, + SectionOutputIcon, +} from './icons'; + +export type SectionInputData = { + remappedId: string; + targetId: NextOperation; +}; + +export type SectionInputNode = Node; + +export type SectionOutputData = { + outputId: string; +}; + +export type SectionOutputNode = Node; + +export type SectionBufferData = SectionInputData; + +export type SectionBufferNode = Node; + +export type SectionInterfaceNode = + | SectionInputNode + | SectionOutputNode + | SectionBufferNode; + +export function SectionNodeComp(props: NodeProps>) { + const label = isSectionBuilder(props.data.op) + ? props.data.op.builder + : props.data.op.template; + return ( + } + label={label || 'Select Section'} + variant="inputOutput" + inputHandleType={HandleType.DataBuffer} + /> + ); +} + +export default SectionNodeComp; + +export type SectionInterfaceNodeTypes = + | 'sectionInput' + | 'sectionOutput' + | 'sectionBuffer'; + +export function SectionInputNodeComp(props: NodeProps) { + return ( + } + label="Section Input" + variant="output" + caption={props.data.remappedId} + /> + ); +} + +export function SectionOutputNodeComp(props: NodeProps) { + return ( + } + label="Section Output" + variant="input" + caption={props.data.outputId} + /> + ); +} + +export function SectionBufferNodeComp(props: NodeProps) { + return ( + } + label="Section Buffer" + variant="output" + caption={props.data.remappedId} + outputHandleType={HandleType.Buffer} + /> + ); +} diff --git a/diagram-editor/frontend/nodes/serialized-join-node.tsx b/diagram-editor/frontend/nodes/serialized-join-node.tsx new file mode 100644 index 00000000..8ac7c52d --- /dev/null +++ b/diagram-editor/frontend/nodes/serialized-join-node.tsx @@ -0,0 +1,19 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { SerializedJoinIcon } from './icons'; + +function SerializedJoinNodeComp( + props: NodeProps>, +) { + return ( + } + label="Serialized Join" + variant="inputOutput" + /> + ); +} + +export default SerializedJoinNodeComp; diff --git a/diagram-editor/frontend/nodes/split-node.tsx b/diagram-editor/frontend/nodes/split-node.tsx new file mode 100644 index 00000000..b21aed82 --- /dev/null +++ b/diagram-editor/frontend/nodes/split-node.tsx @@ -0,0 +1,17 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { SplitIcon } from './icons'; + +function SplitNodeComp(props: NodeProps>) { + return ( + } + label="Split" + variant="inputOutput" + /> + ); +} + +export default SplitNodeComp; diff --git a/diagram-editor/frontend/nodes/start-node.tsx b/diagram-editor/frontend/nodes/start-node.tsx new file mode 100644 index 00000000..cb659568 --- /dev/null +++ b/diagram-editor/frontend/nodes/start-node.tsx @@ -0,0 +1,33 @@ +import { Button, Paper } from '@mui/material'; +import type { NodeProps } from '@xyflow/react'; +import { Handle, Position } from '@xyflow/react'; +import { LAYOUT_OPTIONS } from '../utils/layout'; +import type { BuiltinNode } from '.'; + +function StartNodeComp({ + isConnectable, + sourcePosition = Position.Bottom, +}: NodeProps) { + return ( + + + + + ); +} + +export default StartNodeComp; diff --git a/diagram-editor/frontend/nodes/stream-out-node.tsx b/diagram-editor/frontend/nodes/stream-out-node.tsx new file mode 100644 index 00000000..847beb6b --- /dev/null +++ b/diagram-editor/frontend/nodes/stream-out-node.tsx @@ -0,0 +1,19 @@ +import type { NodeProps } from '@xyflow/react'; +import { HandleType } from '../handles'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { StreamOutIcon } from './icons'; + +function StreamOutNodeComp(props: NodeProps>) { + return ( + } + label="StreamOut" + variant="input" + inputHandleType={HandleType.Stream} + /> + ); +} + +export default StreamOutNodeComp; diff --git a/diagram-editor/frontend/nodes/terminate-node.tsx b/diagram-editor/frontend/nodes/terminate-node.tsx new file mode 100644 index 00000000..9cb7b342 --- /dev/null +++ b/diagram-editor/frontend/nodes/terminate-node.tsx @@ -0,0 +1,32 @@ +import { Button, Paper } from '@mui/material'; +import { Handle, type NodeProps, Position } from '@xyflow/react'; +import { LAYOUT_OPTIONS } from '../utils/layout'; +import type { BuiltinNode } from '.'; + +function TerminateNodeComp({ + isConnectable, + targetPosition = Position.Top, +}: NodeProps) { + return ( + + + + + ); +} + +export default TerminateNodeComp; diff --git a/diagram-editor/frontend/nodes/transform-node.tsx b/diagram-editor/frontend/nodes/transform-node.tsx new file mode 100644 index 00000000..7bc1f1c2 --- /dev/null +++ b/diagram-editor/frontend/nodes/transform-node.tsx @@ -0,0 +1,17 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { TransformIcon } from './icons'; + +function TransformNodeComp(props: NodeProps>) { + return ( + } + label="Transform" + variant="inputOutput" + /> + ); +} + +export default TransformNodeComp; diff --git a/diagram-editor/frontend/nodes/unzip-node.tsx b/diagram-editor/frontend/nodes/unzip-node.tsx new file mode 100644 index 00000000..76c3a08b --- /dev/null +++ b/diagram-editor/frontend/nodes/unzip-node.tsx @@ -0,0 +1,17 @@ +import type { NodeProps } from '@xyflow/react'; +import type { OperationNode } from '.'; +import BaseNode from './base-node'; +import { UnzipIcon } from './icons'; + +function UnzipNodeComp(props: NodeProps>) { + return ( + } + label="Unzip" + variant="inputOutput" + /> + ); +} + +export default UnzipNodeComp; diff --git a/diagram-editor/frontend/nodes/utils.ts b/diagram-editor/frontend/nodes/utils.ts new file mode 100644 index 00000000..29a8b5d6 --- /dev/null +++ b/diagram-editor/frontend/nodes/utils.ts @@ -0,0 +1,91 @@ +import type { + BuiltinNode, + BuiltinNodeTypes, + DiagramEditorNode, + OperationNode, + OperationNodeTypes, +} from '.'; +import type { + SectionBufferNode, + SectionInputNode, + SectionInterfaceNode, + SectionOutputNode, +} from './section-node'; + +export function isOperationData( + data: DiagramEditorNode['data'], +): data is OperationNode['data'] { + return 'type' in data; +} + +const OPERATION_NODE_TYPES = Object.keys({ + buffer: null, + buffer_access: null, + fork_clone: null, + fork_result: null, + join: null, + listen: null, + node: null, + scope: null, + section: null, + serialized_join: null, + split: null, + stream_out: null, + transform: null, + unzip: null, +} satisfies Record); + +export function isOperationNode( + node: DiagramEditorNode, +): node is OperationNode { + return OPERATION_NODE_TYPES.includes(node.type); +} + +const BUILTIN_NODE_TYPES = Object.keys({ + start: null, + terminate: null, +} satisfies Record); + +export function isBuiltinNode(node: DiagramEditorNode): node is BuiltinNode { + return BUILTIN_NODE_TYPES.includes(node.type); +} + +export function isScopeNode( + node: DiagramEditorNode, +): node is OperationNode<'scope'> { + return node.type === 'scope'; +} + +export function isSectionInterfaceNode( + node: DiagramEditorNode, +): node is SectionInterfaceNode { + return ( + node.type === 'sectionInput' || + node.type === 'sectionOutput' || + node.type === 'sectionBuffer' + ); +} + +export function isSectionNode( + node: DiagramEditorNode, +): node is OperationNode<'section'> { + return node.type === 'section'; +} + +export function isSectionInputNode( + node: DiagramEditorNode, +): node is SectionInputNode { + return node.type === 'sectionInput'; +} + +export function isSectionOutputNode( + node: DiagramEditorNode, +): node is SectionOutputNode { + return node.type === 'sectionOutput'; +} + +export function isSectionBufferNode( + node: DiagramEditorNode, +): node is SectionBufferNode { + return node.type === 'sectionBuffer'; +} diff --git a/diagram-editor/frontend/registry-provider.tsx b/diagram-editor/frontend/registry-provider.tsx new file mode 100644 index 00000000..fda56bc5 --- /dev/null +++ b/diagram-editor/frontend/registry-provider.tsx @@ -0,0 +1,93 @@ +import { Box, CircularProgress, Typography } from '@mui/material'; +import { + createContext, + type PropsWithChildren, + useContext, + useEffect, + useState, +} from 'react'; +import { timer } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { useApiClient } from './api-client-provider'; +import type { DiagramElementRegistry } from './types/api'; + +const RegistryContextComp = createContext(null); + +export const RegistryProvider = ({ children }: PropsWithChildren) => { + const [registry, setRegistry] = useState(null); + const [showLoading, setShowLoading] = useState(false); + const [error, setError] = useState(null); + const apiClient = useApiClient(); + + useEffect(() => { + const registry$ = apiClient.getRegistry(); + + const timer$ = timer(1000); + + const timerSubscription = timer$ + .pipe(takeUntil(registry$)) + .subscribe(() => { + setShowLoading(true); + }); + + const fetchSubscription = registry$.subscribe({ + next: setRegistry, + error: (err) => { + console.error(err); + setError(err as Error); + }, + }); + + return () => { + timerSubscription.unsubscribe(); + fetchSubscription.unsubscribe(); + }; + }, [apiClient]); + + if (error) { + return ( + + Failed to fetch registry + + ); + } + + if (!registry) { + if (showLoading) { + return ( + + + + ); + } + return null; + } + + return ( + + {children} + + ); +}; + +export const useRegistry = () => { + const context = useContext(RegistryContextComp); + if (!context) { + throw new Error('useRegistry must be used within a RegistryProvider'); + } + return context; +}; diff --git a/diagram-editor/frontend/run-button.tsx b/diagram-editor/frontend/run-button.tsx new file mode 100644 index 00000000..a9e88671 --- /dev/null +++ b/diagram-editor/frontend/run-button.tsx @@ -0,0 +1,171 @@ +import { + Button, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Popover, + Stack, + TextField, + Tooltip, + Typography, + useTheme, +} from '@mui/material'; +import { useMemo, useRef, useState } from 'react'; +import { useApiClient } from './api-client-provider'; +import { useNodeManager } from './node-manager'; +import { MaterialSymbol } from './nodes'; +import { useTemplates } from './templates-provider'; +import { useEdges } from './use-edges'; +import { exportDiagram } from './utils/export-diagram'; + +type ResponseContent = { raw: string } | { err: string }; + +export function RunButton() { + const nodeManager = useNodeManager(); + const edges = useEdges(); + const [openPopover, setOpenPopover] = useState(false); + const buttonRef = useRef(null); + const theme = useTheme(); + const [requestJson, setRequestJson] = useState(''); + const [responseContent, setResponseContent] = useState({ + raw: '', + }); + const apiClient = useApiClient(); + const [templates, _setTemplates] = useTemplates(); + const [running, setRunning] = useState(false); + + const requestError = useMemo(() => { + try { + JSON.parse(requestJson); + return false; + } catch { + return true; + } + }, [requestJson]); + + const responseError = useMemo(() => { + return 'err' in responseContent; + }, [responseContent]); + + const responseValue = useMemo(() => { + if ('err' in responseContent) { + return `Error: ${responseContent.err}`; + } else { + return responseContent.raw; + } + }, [responseContent]); + + const handleRunClick = () => { + try { + const request = JSON.parse(requestJson); + const diagram = exportDiagram(nodeManager, edges, templates); + apiClient.postRunWorkflow(diagram, request).subscribe({ + next: (response) => { + setResponseContent({ raw: JSON.stringify(response, null, 2) }); + setRunning(false); + }, + error: (err) => { + setResponseContent({ err: (err as Error).message }); + setRunning(false); + }, + }); + setRunning(true); + } catch (e) { + setResponseContent({ err: (e as Error).message }); + } + }; + + return ( + <> + + + + setOpenPopover(false)} + anchorEl={buttonRef.current} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + slotProps={{ + paper: { + sx: { + overflow: 'visible', + mt: 0.5, + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + '&:before': { + content: '""', + position: 'absolute', + top: 0, + left: '50%', + transform: 'translateY(-50%) translateX(-50%) rotate(45deg)', + width: 16, + height: 16, + backgroundColor: theme.palette.background.paper, + backgroundImage: 'inherit', + borderTop: `1px solid ${theme.palette.divider}`, + borderLeft: `1px solid ${theme.palette.divider}`, + }, + }, + }, + }} + > + Run Workflow + + + + Request: + setRequestJson(e.target.value)} + error={requestError} + sx={{ backgroundColor: theme.palette.background.paper }} + /> + Response: + + + + + + + + + ); +} diff --git a/diagram-editor/frontend/templates-provider.tsx b/diagram-editor/frontend/templates-provider.tsx new file mode 100644 index 00000000..ec099e0c --- /dev/null +++ b/diagram-editor/frontend/templates-provider.tsx @@ -0,0 +1,36 @@ +import { + createContext, + type Dispatch, + type PropsWithChildren, + type SetStateAction, + useContext, + useState, +} from 'react'; +import type { SectionTemplate } from './types/api'; + +export type TemplatesContext = [ + Record, + Dispatch>>, +]; + +const TemplatesContextComp = createContext(null); + +export function TemplatesProvider({ children }: PropsWithChildren) { + const [templates, setTemplates] = useState>( + {}, + ); + + return ( + + {children} + + ); +} + +export function useTemplates(): TemplatesContext { + const context = useContext(TemplatesContextComp); + if (!context) { + throw new Error('useTemplates must be used within a TemplatesProvider'); + } + return context; +} diff --git a/diagram-editor/frontend/types/api.d.ts b/diagram-editor/frontend/types/api.d.ts new file mode 100644 index 00000000..bc683fe7 --- /dev/null +++ b/diagram-editor/frontend/types/api.d.ts @@ -0,0 +1,1179 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "BufferSelection". + */ +export type BufferSelection = + | { + [k: string]: NextOperation; + } + | NextOperation[]; +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "NextOperation". + */ +export type NextOperation = + | string + | { + builtin: BuiltinTarget; + [k: string]: unknown; + } + | NamespacedOperation; +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "BuiltinTarget". + */ +export type BuiltinTarget = 'terminate' | 'dispose' | 'cancel'; +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "TraceToggle". + */ +export type TraceToggle = 'off' | 'on' | 'messages'; +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "RetentionPolicy". + */ +export type RetentionPolicy = + | { + keep_last: number; + } + | { + keep_first: number; + } + | 'keep_all'; +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "DebugSessionMessage". + */ +export type DebugSessionMessage = + | ({ + operationStarted: string; + [k: string]: unknown; + } & { + type: 'feedback'; + [k: string]: unknown; + }) + | (( + | { + ok: unknown; + [k: string]: unknown; + } + | { + err: string; + [k: string]: unknown; + } + ) & { + type: 'finish'; + [k: string]: unknown; + }); +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "DiagramOperation". + */ +export type DiagramOperation = + | (NodeSchema & { + type: 'node'; + [k: string]: unknown; + }) + | (SectionSchema & { + type: 'section'; + [k: string]: unknown; + }) + | (ScopeSchema & { + type: 'scope'; + [k: string]: unknown; + }) + | (StreamOutSchema & { + type: 'stream_out'; + [k: string]: unknown; + }) + | (ForkCloneSchema & { + type: 'fork_clone'; + [k: string]: unknown; + }) + | (UnzipSchema & { + type: 'unzip'; + [k: string]: unknown; + }) + | (ForkResultSchema & { + type: 'fork_result'; + [k: string]: unknown; + }) + | (SplitSchema & { + type: 'split'; + [k: string]: unknown; + }) + | (JoinSchema & { + type: 'join'; + [k: string]: unknown; + }) + | (SerializedJoinSchema & { + type: 'serialized_join'; + [k: string]: unknown; + }) + | (TransformSchema & { + type: 'transform'; + [k: string]: unknown; + }) + | (BufferSchema & { + type: 'buffer'; + [k: string]: unknown; + }) + | (BufferAccessSchema & { + type: 'buffer_access'; + [k: string]: unknown; + }) + | (ListenSchema & { + type: 'listen'; + [k: string]: unknown; + }); +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "SectionSchema". + */ +export type SectionSchema = ( + | { + builder: string; + [k: string]: unknown; + } + | { + template: string; + [k: string]: unknown; + } +) & { + config?: { + [k: string]: unknown; + }; + connect?: { + [k: string]: NextOperation; + }; + /** + * Override for text that should be displayed for an operation within an + * editor. + */ + display_text?: string | null; + trace?: TraceToggle | null; + [k: string]: unknown; +}; +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "InputRemapping". + */ +export type InputRemapping = + | string[] + | { + [k: string]: NextOperation; + }; +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "Schema". + */ +export type Schema = + | { + [k: string]: unknown; + } + | boolean; + +export interface DiagramEditorApi { + [k: string]: unknown; +} +/** + * Zip a message together with access to one or more buffers. + * + * The receiving node must have an input type of `(Message, Keys)` + * where `Keys` implements the [`Accessor`][1] trait. + * + * [1]: crate::Accessor + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "fork_clone", + * "ops": { + * "fork_clone": { + * "type": "fork_clone", + * "next": ["num_output", "string_output"] + * }, + * "num_output": { + * "type": "node", + * "builder": "num_output", + * "next": "buffer_access" + * }, + * "string_output": { + * "type": "node", + * "builder": "string_output", + * "next": "string_buffer" + * }, + * "string_buffer": { + * "type": "buffer" + * }, + * "buffer_access": { + * "type": "buffer_access", + * "buffers": ["string_buffer"], + * "target_node": "with_buffer_access", + * "next": "with_buffer_access" + * }, + * "with_buffer_access": { + * "type": "node", + * "builder": "with_buffer_access", + * "next": { "builtin": "terminate" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "BufferAccessSchema". + */ +export interface BufferAccessSchema { + buffers: BufferSelection; + /** + * Override for text that should be displayed for an operation within an + * editor. + */ + display_text?: string | null; + next: NextOperation; + trace?: TraceToggle | null; + [k: string]: unknown; +} +/** + * Refer to an operation inside of a namespace, e.g. { "": "" + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "NamespacedOperation". + */ +export interface NamespacedOperation { + [k: string]: string; +} +/** + * Create a [`Buffer`][1] which can be used to store and pull data within + * a scope. + * + * By default the [`BufferSettings`][2] will keep the single last message + * pushed to the buffer. You can change that with the optional `settings` + * property. + * + * Use the `"serialize": true` option to serialize the messages into + * [`JsonMessage`] before they are inserted into the buffer. This + * allows any serializable message type to be pushed into the buffer. If + * left unspecified, the buffer will store the specific data type that gets + * pushed into it. If the buffer inputs are not being serialized, then all + * incoming messages being pushed into the buffer must have the same type. + * + * [1]: crate::Buffer + * [2]: crate::BufferSettings + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "fork_clone", + * "ops": { + * "fork_clone": { + * "type": "fork_clone", + * "next": ["num_output", "string_output", "all_num_buffer", "serialized_num_buffer"] + * }, + * "num_output": { + * "type": "node", + * "builder": "num_output", + * "next": "buffer_access" + * }, + * "string_output": { + * "type": "node", + * "builder": "string_output", + * "next": "string_buffer" + * }, + * "string_buffer": { + * "type": "buffer", + * "settings": { + * "retention": { "keep_last": 10 } + * } + * }, + * "all_num_buffer": { + * "type": "buffer", + * "settings": { + * "retention": "keep_all" + * } + * }, + * "serialized_num_buffer": { + * "type": "buffer", + * "serialize": true + * }, + * "buffer_access": { + * "type": "buffer_access", + * "buffers": ["string_buffer"], + * "target_node": "with_buffer_access", + * "next": "with_buffer_access" + * }, + * "with_buffer_access": { + * "type": "node", + * "builder": "with_buffer_access", + * "next": { "builtin": "terminate" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "BufferSchema". + */ +export interface BufferSchema { + /** + * Override for text that should be displayed for an operation within an + * editor. + */ + display_text?: string | null; + /** + * If true, messages will be serialized before sending into the buffer. + */ + serialize?: boolean | null; + settings?: BufferSettings; + trace?: TraceToggle | null; + [k: string]: unknown; +} +/** + * Settings to describe the behavior of a buffer. + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "BufferSettings". + */ +export interface BufferSettings { + retention: RetentionPolicy; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "Diagram". + */ +export interface Diagram { + default_trace?: TraceToggle; + on_implicit_error?: NextOperation | null; + /** + * Operations that define the workflow + */ + ops: { + [k: string]: DiagramOperation; + }; + start: NextOperation; + templates?: { + [k: string]: SectionTemplate; + }; + /** + * Version of the diagram, should always be `0.1.0`. + */ + version: string; + [k: string]: unknown; +} +/** + * Create an operation that that takes an input message and produces an + * output message. + * + * The behavior is determined by the choice of node `builder` and + * optioanlly the `config` that you provide. Each type of node builder has + * its own schema for the config. + * + * The output message will be sent to the operation specified by `next`. + * + * TODO(@mxgrey): [Support stream outputs](https://github.com/open-rmf/bevy_impulse/issues/43) + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "cutting_board", + * "ops": { + * "cutting_board": { + * "type": "node", + * "builder": "chop", + * "config": "diced", + * "next": "bowl" + * }, + * "bowl": { + * "type": "node", + * "builder": "stir", + * "next": "oven" + * }, + * "oven": { + * "type": "node", + * "builder": "bake", + * "config": { + * "temperature": 200, + * "duration": 120 + * }, + * "next": { "builtin": "terminate" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "NodeSchema". + */ +export interface NodeSchema { + builder: string; + config?: unknown; + /** + * Override for text that should be displayed for an operation within an + * editor. + */ + display_text?: string | null; + next: NextOperation; + stream_out?: { + [k: string]: NextOperation; + }; + trace?: TraceToggle | null; + [k: string]: unknown; +} +/** + * Create a scope which will function like its own encapsulated workflow + * within the paren workflow. Each message that enters a scope will trigger + * a new independent session for that scope to begin running with the incoming + * message itself being the input message of the scope. When multiple sessions + * for the same scope are running, they cannot see or interfere with each other. + * + * Once a session terminates, the scope will send the terminating message as + * its output. Scopes can use the `stream_out` operation to stream messages out + * to the parent workflow while running. + * + * Scopes have two common uses: + * * isolate - Prevent simultaneous runs of the same workflow components + * (especially buffers) from interfering with each other. + * * race - Run multiple branches simultaneously inside the scope and race + * them against each ohter. The first branch that reaches the scope's + * terminate operation "wins" the race, and only its output will continue + * on in the parent workflow. All other branches will be disposed. + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "approach_door", + * "ops": { + * "approach_door": { + * "type": "scope", + * "start": "begin", + * "ops": { + * "begin": { + * "type": "fork_clone", + * "next": [ + * "move_to_door", + * "detect_door_proximity" + * ] + * }, + * "move_to_door": { + * "type": "node", + * "builder": "move", + * "config": { + * "place": "L1_north_lobby_outside" + * }, + * "next": { "builtin" : "terminate" } + * }, + * "detect_proximity": { + * "type": "node", + * "builder": "detect_proximity", + * "config": { + * "type": "door", + * "name": "L1_north_lobby" + * }, + * "next": { "builtin" : "terminate" } + * } + * }, + * "next": { "builtin" : "try_open_door" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "ScopeSchema". + */ +export interface ScopeSchema { + next: NextOperation; + on_implicit_error?: NextOperation | null; + /** + * Operations that exist inside this scope. + */ + ops: { + [k: string]: DiagramOperation; + }; + settings?: ScopeSettings; + start: NextOperation; + /** + * Where to connect streams that are coming out of this scope. + */ + stream_out?: { + [k: string]: NextOperation; + }; + [k: string]: unknown; +} +/** + * Settings which determine how the top-level scope of the workflow behaves. + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "ScopeSettings". + */ +export interface ScopeSettings { + /** + * Should we prevent the scope from being interrupted (e.g. cancelled)? + * False by default, meaning by default scopes can be cancelled or + * interrupted. + */ + uninterruptible: boolean; + [k: string]: unknown; +} +/** + * Declare a stream output for the current scope. Outputs that you connect + * to this operation will be streamed out of the scope that this operation + * is declared in. + * + * For the root-level scope, make sure you use a stream pack that is + * compatible with all stream out operations that you declare, otherwise + * you may get a connection error at runtime. + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "plan", + * "ops": { + * "progress_stream": { + * "type": "stream_out", + * "name": "progress" + * }, + * "plan": { + * "type": "node", + * "builder": "planner", + * "next": "drive", + * "stream_out" : { + * "progress": "progress_stream" + * } + * }, + * "drive": { + * "type": "node", + * "builder": "navigation", + * "next": { "builtin": "terminate" }, + * "stream_out": { + * "progress": "progress_stream" + * } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "StreamOutSchema". + */ +export interface StreamOutSchema { + /** + * The name of the stream exiting the workflow or scope. + */ + name: string; + [k: string]: unknown; +} +/** + * If the request is cloneable, clone it into multiple responses that can + * each be sent to a different operation. The `next` property is an array. + * + * This creates multiple simultaneous branches of execution within the + * workflow. Usually when you have multiple branches you will either + * * race - connect all branches to `terminate` and the first branch to + * finish "wins" the race and gets to the be output + * * join - connect each branch into a buffer and then use the `join` + * operation to reunite them + * * collect - TODO(@mxgrey): [add the collect operation](https://github.com/open-rmf/bevy_impulse/issues/59) + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "begin_race", + * "ops": { + * "begin_race": { + * "type": "fork_clone", + * "next": [ + * "ferrari", + * "mustang" + * ] + * }, + * "ferrari": { + * "type": "node", + * "builder": "drive", + * "config": "ferrari", + * "next": { "builtin": "terminate" } + * }, + * "mustang": { + * "type": "node", + * "builder": "drive", + * "config": "mustang", + * "next": { "builtin": "terminate" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "ForkCloneSchema". + */ +export interface ForkCloneSchema { + /** + * Override for text that should be displayed for an operation within an + * editor. + */ + display_text?: string | null; + next: NextOperation[]; + trace?: TraceToggle | null; + [k: string]: unknown; +} +/** + * If the input message is a tuple of (T1, T2, T3, ...), unzip it into + * multiple output messages of T1, T2, T3, ... + * + * Each output message may have a different type and can be sent to a + * different operation. This creates multiple simultaneous branches of + * execution within the workflow. See [`DiagramOperation::ForkClone`] for + * more information on parallel branches. + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "name_phone_address", + * "ops": { + * "name_phone_address": { + * "type": "unzip", + * "next": [ + * "process_name", + * "process_phone_number", + * "process_address" + * ] + * }, + * "process_name": { + * "type": "node", + * "builder": "process_name", + * "next": "name_processed" + * }, + * "process_phone_number": { + * "type": "node", + * "builder": "process_phone_number", + * "next": "phone_number_processed" + * }, + * "process_address": { + * "type": "node", + * "builder": "process_address", + * "next": "address_processed" + * }, + * "name_processed": { "type": "buffer" }, + * "phone_number_processed": { "type": "buffer" }, + * "address_processed": { "type": "buffer" }, + * "finished": { + * "type": "join", + * "buffers": [ + * "name_processed", + * "phone_number_processed", + * "address_processed" + * ], + * "next": { "builtin": "terminate" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "UnzipSchema". + */ +export interface UnzipSchema { + /** + * Override for text that should be displayed for an operation within an + * editor. + */ + display_text?: string | null; + next: NextOperation[]; + trace?: TraceToggle | null; + [k: string]: unknown; +} +/** + * If the request is a [`Result`], send the output message down an + * `ok` branch or down an `err` branch depending on whether the result has + * an [`Ok`] or [`Err`] value. The `ok` branch will receive a `T` while the + * `err` branch will receive an `E`. + * + * Only one branch will be activated by each input message that enters the + * operation. + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "fork_result", + * "ops": { + * "fork_result": { + * "type": "fork_result", + * "ok": { "builtin": "terminate" }, + * "err": { "builtin": "dispose" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "ForkResultSchema". + */ +export interface ForkResultSchema { + /** + * Override for text that should be displayed for an operation within an + * editor. + */ + display_text?: string | null; + err: NextOperation; + ok: NextOperation; + trace?: TraceToggle | null; + [k: string]: unknown; +} +/** + * If the input message is a list-like or map-like object, split it into + * multiple output messages. + * + * Note that the type of output message from the split depends on how the + * input message implements the [`Splittable`][1] trait. In many cases this + * will be a tuple of `(key, value)`. + * + * There are three ways to specify where the split output messages should + * go, and all can be used at the same time: + * * `sequential` - For array-like collections, send the "first" element of + * the collection to the first operation listed in the `sequential` array. + * The "second" element of the collection goes to the second operation + * listed in the `sequential` array. And so on for all elements in the + * collection. If one of the elements in the collection is mentioned in + * the `keyed` set, then the sequence will pass over it as if the element + * does not exist at all. + * * `keyed` - For map-like collections, send the split element associated + * with the specified key to its associated output. + * * `remaining` - Any elements that are were not captured by `sequential` + * or by `keyed` will be sent to this. + * + * [1]: crate::Splittable + * + * # Examples + * + * Suppose I am an animal rescuer sorting through a new collection of + * animals that need recuing. My home has space for three exotic animals + * plus any number of dogs and cats. + * + * I have a custom `SpeciesCollection` data structure that implements + * [`Splittable`][1] by allowing you to key on the type of animal. + * + * In the workflow below, we send all cats and dogs to `home`, and we also + * send the first three non-dog and non-cat species to `home`. All + * remaining animals go to the zoo. + * + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "select_animals", + * "ops": { + * "select_animals": { + * "type": "split", + * "sequential": [ + * "home", + * "home", + * "home" + * ], + * "keyed": { + * "cat": "home", + * "dog": "home" + * }, + * "remaining": "zoo" + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * If we input `["frog", "cat", "bear", "beaver", "dog", "rabbit", "dog", "monkey"]` + * then `frog`, `bear`, and `beaver` will be sent to `home` since those are + * the first three animals that are not `dog` or `cat`, and we will also + * send one `cat` and two `dog` home. `rabbit` and `monkey` will be sent to the zoo. + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "SplitSchema". + */ +export interface SplitSchema { + /** + * Override for text that should be displayed for an operation within an + * editor. + */ + display_text?: string | null; + keyed?: { + [k: string]: NextOperation; + }; + remaining?: NextOperation | null; + sequential?: NextOperation[]; + trace?: TraceToggle | null; + [k: string]: unknown; +} +/** + * Wait for exactly one item to be available in each buffer listed in + * `buffers`, then join each of those items into a single output message + * that gets sent to `next`. + * + * If the `next` operation is not a `node` type (e.g. `fork_clone`) then + * you must specify a `target_node` so that the diagram knows what data + * structure to join the values into. + * + * The output message type must be registered as joinable at compile time. + * If you want to join into a dynamic data structure then you should use + * [`DiagramOperation::SerializedJoin`] instead. + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "begin_measuring", + * "ops": { + * "begin_measuring": { + * "type": "fork_clone", + * "next": ["localize", "imu"] + * }, + * "localize": { + * "type": "node", + * "builder": "localize", + * "next": "estimated_position" + * }, + * "imu": { + * "type": "node", + * "builder": "imu", + * "config": "velocity", + * "next": "estimated_velocity" + * }, + * "estimated_position": { "type": "buffer" }, + * "estimated_velocity": { "type": "buffer" }, + * "gather_state": { + * "type": "join", + * "buffers": { + * "position": "estimate_position", + * "velocity": "estimate_velocity" + * }, + * "next": "report_state" + * }, + * "report_state": { + * "type": "node", + * "builder": "publish_state", + * "next": { "builtin": "terminate" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "JoinSchema". + */ +export interface JoinSchema { + buffers: BufferSelection; + next: NextOperation; + [k: string]: unknown; +} +/** + * Same as [`DiagramOperation::Join`] but all input messages must be + * serializable, and the output message will always be [`serde_json::Value`]. + * + * If you use an array for `buffers` then the output message will be a + * [`serde_json::Value::Array`]. If you use a map for `buffers` then the + * output message will be a [`serde_json::Value::Object`]. + * + * Unlike [`DiagramOperation::Join`], the `target_node` property does not + * exist for this schema. + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "SerializedJoinSchema". + */ +export interface SerializedJoinSchema { + buffers: BufferSelection; + next: NextOperation; + [k: string]: unknown; +} +/** + * If the request is serializable, transform it by running it through a [CEL](https://cel.dev/) program. + * The context includes a "request" variable which contains the input message. + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "transform", + * "ops": { + * "transform": { + * "type": "transform", + * "cel": "request.name", + * "next": { "builtin": "terminate" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * Note that due to how `serde_json` performs serialization, positive integers are always + * serialized as unsigned. In CEL, You can't do an operation between unsigned and signed so + * it is recommended to always perform explicit casts. + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "transform", + * "ops": { + * "transform": { + * "type": "transform", + * "cel": "int(request.score) * 3", + * "next": { "builtin": "terminate" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * ``` + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "TransformSchema". + */ +export interface TransformSchema { + cel: string; + /** + * Override for text that should be displayed for an operation within an + * editor. + */ + display_text?: string | null; + next: NextOperation; + on_error?: NextOperation | null; + trace?: TraceToggle | null; + [k: string]: unknown; +} +/** + * Listen on a buffer. + * + * # Examples + * ``` + * # bevy_impulse::Diagram::from_json_str(r#" + * { + * "version": "0.1.0", + * "start": "num_output", + * "ops": { + * "buffer": { + * "type": "buffer" + * }, + * "num_output": { + * "type": "node", + * "builder": "num_output", + * "next": "buffer" + * }, + * "listen": { + * "type": "listen", + * "buffers": ["buffer"], + * "target_node": "listen_buffer", + * "next": "listen_buffer" + * }, + * "listen_buffer": { + * "type": "node", + * "builder": "listen_buffer", + * "next": { "builtin": "terminate" } + * } + * } + * } + * # "#)?; + * # Ok::<_, serde_json::Error>(()) + * + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "ListenSchema". + */ +export interface ListenSchema { + buffers: BufferSelection; + next: NextOperation; + target_node?: NextOperation | null; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "SectionTemplate". + */ +export interface SectionTemplate { + buffers?: InputRemapping; + inputs?: InputRemapping; + /** + * Operations that define the behavior of the section. + */ + ops: { + [k: string]: DiagramOperation; + }; + /** + * These are the outputs that the section is exposing so you can connect + * them into siblings of the section. + */ + outputs?: string[]; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "DiagramElementRegistry". + */ +export interface DiagramElementRegistry { + messages: { + [k: string]: MessageRegistration; + }; + nodes: { + [k: string]: NodeRegistration; + }; + schemas: { + [k: string]: unknown; + }; + sections: { + [k: string]: SectionRegistration; + }; + trace_supported: boolean; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "MessageRegistration". + */ +export interface MessageRegistration { + operations: MessageOperation; + schema?: Schema | null; + type_name: string; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "MessageOperation". + */ +export interface MessageOperation { + deserialize?: { + [k: string]: unknown; + } | null; + fork_clone?: { + [k: string]: unknown; + } | null; + fork_result?: { + [k: string]: unknown; + } | null; + join?: { + [k: string]: unknown; + } | null; + serialize?: { + [k: string]: unknown; + } | null; + split?: { + [k: string]: unknown; + } | null; + unzip?: string[] | null; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "NodeRegistration". + */ +export interface NodeRegistration { + config_schema: Schema; + /** + * If the user does not specify a default display text, the node ID will + * be used here. + */ + default_display_text: string; + request: string; + response: string; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "SectionRegistration". + */ +export interface SectionRegistration { + config_schema: Schema; + default_display_text: string; + metadata: SectionMetadata; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "SectionMetadata". + */ +export interface SectionMetadata { + buffers: { + [k: string]: SectionBuffer; + }; + inputs: { + [k: string]: SectionInput; + }; + outputs: { + [k: string]: SectionOutput; + }; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "SectionBuffer". + */ +export interface SectionBuffer { + item_type?: string | null; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "SectionInput". + */ +export interface SectionInput { + message_type: string; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "SectionOutput". + */ +export interface SectionOutput { + message_type: string; + [k: string]: unknown; +} +/** + * This interface was referenced by `DiagramEditorApi`'s JSON-Schema + * via the `definition` "PostRunRequest". + */ +export interface PostRunRequest { + diagram: Diagram; + request: unknown; + [k: string]: unknown; +} diff --git a/diagram-editor/frontend/types/react-flow.ts b/diagram-editor/frontend/types/react-flow.ts new file mode 100644 index 00000000..39715de7 --- /dev/null +++ b/diagram-editor/frontend/types/react-flow.ts @@ -0,0 +1,20 @@ +import type { + Edge as ReactFlowEdge, + Node as ReactFlowNode, +} from '@xyflow/react'; +import type { HandleId } from '../handles'; + +export type Node< + D extends Record, + T extends string, +> = ReactFlowNode & Pick, 'type' | 'data'>; + +export type Edge< + O extends Record, + I extends { type: string }, + T extends string, +> = ReactFlowEdge<{ output: O; input: I }, T> & + Pick, 'type' | 'data'> & { + sourceHandle?: HandleId; + targetHandle?: HandleId; + }; diff --git a/diagram-editor/frontend/use-edges.ts b/diagram-editor/frontend/use-edges.ts new file mode 100644 index 00000000..8397f47a --- /dev/null +++ b/diagram-editor/frontend/use-edges.ts @@ -0,0 +1,10 @@ +import { createContext, useContext } from 'react'; +import type { DiagramEditorEdge } from './edges'; + +const EdgesContext = createContext([]); + +export const EdgesProvider = EdgesContext.Provider; + +export function useEdges() { + return useContext(EdgesContext); +} diff --git a/diagram-editor/frontend/use-react-flow.ts b/diagram-editor/frontend/use-react-flow.ts new file mode 100644 index 00000000..9357a71e --- /dev/null +++ b/diagram-editor/frontend/use-react-flow.ts @@ -0,0 +1,7 @@ +import { useReactFlow as _useReactFlow } from '@xyflow/react'; +import type { DiagramEditorEdge } from './edges'; +import type { DiagramEditorNode } from './nodes'; + +export function useReactFlow() { + return _useReactFlow(); +} diff --git a/diagram-editor/frontend/utils/ajv.ts b/diagram-editor/frontend/utils/ajv.ts new file mode 100644 index 00000000..18ece46c --- /dev/null +++ b/diagram-editor/frontend/utils/ajv.ts @@ -0,0 +1,22 @@ +import Ajv from 'ajv/dist/2020'; +import type { ValidateFunction } from 'ajv/dist/core'; +import addFormats from 'ajv-formats'; +import apiSchema from '../api.preprocessed.schema.json'; + +const ajv = addFormats(new Ajv({ allowUnionTypes: true })).addFormat( + 'uint', + /^[0-9]+$/, +); +ajv.compile(apiSchema); + +export function getSchema( + key: keyof (typeof apiSchema)['$defs'], +): ValidateFunction { + const validate = ajv.getSchema(`#/$defs/${key}`) as ValidateFunction; + if (!validate) { + throw new Error(`cannot validate ${key}`); + } + return validate; +} + +export default ajv; diff --git a/diagram-editor/frontend/utils/auto-layout.ts b/diagram-editor/frontend/utils/auto-layout.ts new file mode 100644 index 00000000..95afb02d --- /dev/null +++ b/diagram-editor/frontend/utils/auto-layout.ts @@ -0,0 +1,151 @@ +import type { NodeChange, Rect } from '@xyflow/react'; +import * as dagre from 'dagre'; +import type { DiagramEditorEdge } from '../edges'; +import { NodeManager } from '../node-manager'; +import { + type DiagramEditorNode, + type OperationNode, + START_ID, + TERMINATE_ID, +} from '../nodes'; +import { calculateScopeBounds, type LayoutOptions } from './layout'; +import { joinNamespaces } from './namespace'; + +function isScopeNode(node: DiagramEditorNode): node is OperationNode<'scope'> { + return node.type === 'scope'; +} + +export function autoLayout( + nodes: DiagramEditorNode[], + edges: DiagramEditorEdge[], + options: LayoutOptions, +): NodeChange[] { + const nodeManager = new NodeManager(nodes); + const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ + rankdir: 'TB', + ranksep: options.nodeHeight, + }); + + const scopeChildrens: Record = {}; + for (const node of nodes) { + const parentNode = node.parentId + ? nodeManager.tryGetNode(node.parentId) + : null; + if (parentNode?.id) { + if (!scopeChildrens[parentNode.id]) { + scopeChildrens[parentNode.id] = []; + } + scopeChildrens[parentNode.id].push(node); + } + + // exclude scope node from auto layout + if (!isScopeNode(node)) { + dagreGraph.setNode(node.id, { + // dagre requires the node dimensions to be known, the easy solution is to only use + // fixed size nodes. The complex alternative is to delay auto layout until ReactFlow + // finishes measuring the nodes, this is complex because + // 1. There is no hook for when ReactFlow finishes measurements + // 2. ReactFlow does not delay updating the DOM until the measurements are complete. + // 2.1. This means that when loading a new diagram, there will be a short period where the layout is not yet computed. + // 3. Measurements of a node may change multiple times before it "stabilizes". + // 3.1. This means that even if all nodes have measurements, they may not be final measurements. + // If the measurements change after the layout is computed, the layout will be wrong. + width: options.nodeWidth, + height: options.nodeHeight, + }); + } + } + + for (const edge of edges) { + const sourceNode = nodeManager.getNode(edge.source); + const targetNode = nodeManager.getNode(edge.target); + + if (targetNode.type === 'scope') { + dagreGraph.setEdge( + edge.source, + nodeManager.getNodeFromNamespaceOpId( + joinNamespaces(targetNode.data.namespace, targetNode.data.opId), + START_ID, + ).id, + { minlen: 2 }, + ); + } else if (sourceNode.type === 'scope') { + dagreGraph.setEdge( + nodeManager.getNodeFromNamespaceOpId( + joinNamespaces(sourceNode.data.namespace, sourceNode.data.opId), + TERMINATE_ID, + ).id, + edge.target, + { minlen: 2 }, + ); + } else { + dagreGraph.setEdge(edge.source, edge.target); + } + } + + dagre.layout(dagreGraph); + + const scopeBounds: Record = {}; + for (const [scopeNodeId, children] of Object.entries(scopeChildrens)) { + scopeBounds[scopeNodeId] = calculateScopeBounds( + children.map((n) => dagreGraph.node(n.id)), + ); + } + + const changes: NodeChange[] = []; + for (const node of nodes) { + const nodePosition = dagreGraph.node(node.id); + if (isScopeNode(node)) { + continue; + } + + if (node.parentId) { + const scope = scopeBounds[node.parentId]; + changes.push({ + type: 'position', + id: node.id, + position: { + x: + nodePosition.x - + // convert position to be relative to scope + scope.x, + y: + nodePosition.y - + // convert position to be relative to scope + scope.y, + }, + }); + } else { + changes.push({ + type: 'position', + id: node.id, + position: { + x: nodePosition.x, + y: nodePosition.y, + }, + }); + } + } + for (const [scopeNodeId, bounds] of Object.entries(scopeBounds)) { + changes.push({ + type: 'position', + id: scopeNodeId, + position: { + x: bounds.x, + y: bounds.y, + }, + }); + changes.push({ + type: 'dimensions', + id: scopeNodeId, + dimensions: { + width: bounds.width, + height: bounds.height, + }, + setAttributes: true, + }); + } + + return changes; +} diff --git a/diagram-editor/frontend/utils/change.ts b/diagram-editor/frontend/utils/change.ts new file mode 100644 index 00000000..2f01ff04 --- /dev/null +++ b/diagram-editor/frontend/utils/change.ts @@ -0,0 +1,8 @@ +import type { NodeChange, NodeRemoveChange } from '@xyflow/react'; +import type { DiagramEditorNode } from '../nodes'; + +export function isRemoveChange( + change: NodeChange, +): change is NodeRemoveChange { + return change.type === 'remove'; +} diff --git a/diagram-editor/frontend/utils/connection.test.ts b/diagram-editor/frontend/utils/connection.test.ts new file mode 100644 index 00000000..46acd2bb --- /dev/null +++ b/diagram-editor/frontend/utils/connection.test.ts @@ -0,0 +1,461 @@ +import { + createBufferEdge, + createDefaultEdge, + createForkResultErrEdge, + createForkResultOkEdge, + type DiagramEditorEdge, +} from '../edges'; +import { NodeManager } from '../node-manager'; +import { + createOperationNode, + createSectionBufferNode, + createSectionInputNode, + createSectionOutputNode, + createTerminateNode, +} from '../nodes'; +import { + getValidEdgeTypes, + validateEdgeQuick, + validateEdgeSimple, +} from './connection'; +import { ROOT_NAMESPACE } from './namespace'; + +describe('validate edges', () => { + test('"buffer" can only connect to operations that accepts a buffer', () => { + const node = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'node', builder: 'test_builder', next: { builtin: 'dispose' } }, + 'test_op_node', + ); + const buffer = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'buffer' }, + 'test_op_buffer', + ); + const join = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { + type: 'join', + buffers: [], + next: { builtin: 'dispose' }, + }, + 'test_op_join', + ); + + { + // "node" does not accept buffer + const validEdges = getValidEdgeTypes(buffer, null, node, null); + expect(validEdges.length).toBe(0); + + // "buffer" does not output data ("default" edge) + const edge = createDefaultEdge(buffer.id, join.id); + const nodeManager = new NodeManager([buffer, join]); + const result = validateEdgeQuick(edge, nodeManager); + expect(result.valid).toBe(false); + } + + { + const validEdges = getValidEdgeTypes(buffer, null, join, null); + expect(validEdges.length).toBe(1); + expect(validEdges).toContain('buffer'); + } + + { + const edge = createBufferEdge(buffer.id, join.id, { + type: 'bufferSeq', + seq: 0, + }); + const nodeManager = new NodeManager([buffer, join]); + const result = validateEdgeQuick(edge, nodeManager); + expect(result.valid).toBe(true); + } + + { + const edge = createBufferEdge(buffer.id, join.id, { + type: 'bufferKey', + key: 'test', + }); + const nodeManager = new NodeManager([buffer, join]); + const result = validateEdgeQuick(edge, nodeManager); + expect(result.valid).toBe(true); + } + }); + + test('"buffer_access" accepts both data and buffer edges', () => { + const nodeNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'node', builder: 'test_builder', next: { builtin: 'dispose' } }, + 'test_op_node', + ); + const bufferNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'buffer' }, + 'test_op_buffer', + ); + const bufferAccessNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'buffer_access', buffers: [], next: { builtin: 'dispose' } }, + 'test_op_buffer_access', + ); + + { + const validEdges = getValidEdgeTypes( + nodeNode, + null, + bufferAccessNode, + null, + ); + expect(validEdges.length).toBe(1); + expect(validEdges).toContain('default'); + } + { + const validEdges = getValidEdgeTypes( + bufferNode, + null, + bufferAccessNode, + null, + ); + expect(validEdges.length).toBe(1); + expect(validEdges).toContain('buffer'); + } + }); + + test('"join" node only accepts buffer edges', () => { + const nodeNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'node', builder: 'test_builder', next: { builtin: 'dispose' } }, + 'test_op_node', + ); + const bufferNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'buffer' }, + 'test_op_buffer', + ); + const joinNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'join', buffers: [], next: { builtin: 'dispose' } }, + 'test_op_join', + ); + const serializedJoinNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'serialized_join', buffers: [], next: { builtin: 'dispose' } }, + 'test_op_serialized_join', + ); + + for (const targetNode of [joinNode, serializedJoinNode]) { + { + const validEdges = getValidEdgeTypes(nodeNode, null, targetNode, null); + expect(validEdges.length).toBe(0); + } + { + const validEdges = getValidEdgeTypes( + bufferNode, + null, + targetNode, + null, + ); + expect(validEdges.length).toBe(1); + expect(validEdges).toContain('buffer'); + } + } + }); + + test('"sectionInput" can only connect to operations that accepts data', () => { + const sectionInput = createSectionInputNode( + 'test_section_input', + 'test_section_input', + { x: 0, y: 0 }, + ); + const node = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'node', builder: 'test_builder', next: { builtin: 'dispose' } }, + 'test_op_node', + ); + const listen = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'listen', buffers: [], next: { builtin: 'dispose' } }, + 'test_op_listen', + ); + + { + const validEdges = getValidEdgeTypes(sectionInput, null, node, null); + expect(validEdges.length).toBe(1); + expect(validEdges).toContain('default'); + } + + { + const validEdges = getValidEdgeTypes(sectionInput, null, listen, null); + expect(validEdges.length).toBe(0); + } + }); + + test('"sectionBuffer" can only connect to operations that accepts buffer', () => { + const sectionBuffer = createSectionBufferNode( + 'test_section_buffer', + 'test_section_buffer', + { x: 0, y: 0 }, + ); + const node = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'node', builder: 'test_builder', next: { builtin: 'dispose' } }, + 'test_op_node', + ); + const listen = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'listen', buffers: [], next: { builtin: 'dispose' } }, + 'test_op_listen', + ); + + { + const validEdges = getValidEdgeTypes(sectionBuffer, null, node, null); + expect(validEdges.length).toBe(0); + } + + { + const validEdges = getValidEdgeTypes(sectionBuffer, null, listen, null); + expect(validEdges.length).toBe(1); + expect(validEdges).toContain('buffer'); + } + }); + + test('"sectionOutput" only accepts data edges', () => { + const sectionOutput = createSectionOutputNode('test_section_output', { + x: 0, + y: 0, + }); + const node = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'node', builder: 'test_builder', next: { builtin: 'dispose' } }, + 'test_op_node', + ); + const buffer = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'buffer', buffers: [] }, + 'test_op_buffer', + ); + + { + const validEdges = getValidEdgeTypes(node, null, sectionOutput, null); + expect(validEdges.length).toBe(1); + expect(validEdges).toContain('default'); + } + + { + const validEdges = getValidEdgeTypes(buffer, null, sectionOutput, null); + expect(validEdges.length).toBe(0); + } + }); + + test('"node" operation only allows 1 output', () => { + const nodeNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'node', builder: 'test_builder', next: { builtin: 'dispose' } }, + 'test_op_node', + ); + const forkCloneNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'fork_clone', next: [] }, + 'test_fork_clone', + ); + const forkCloneNode2 = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'fork_clone', next: [] }, + 'test_fork_clone2', + ); + + const existingEdge = createDefaultEdge(nodeNode.id, forkCloneNode.id); + const nodeManager = new NodeManager([ + nodeNode, + forkCloneNode, + forkCloneNode2, + ]); + const edges = [existingEdge]; + { + const result = validateEdgeSimple(existingEdge, nodeManager, edges); + expect(result.valid).toBe(true); + } + { + const newEdge = createDefaultEdge(nodeNode.id, forkCloneNode2.id); + const result = validateEdgeSimple(newEdge, nodeManager, edges); + expect(result.valid).toBe(false); + } + }); + + test('"fork_clone" operation allows multiple outputs', () => { + const forkCloneNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'fork_clone', next: [] }, + 'test_fork_clone', + ); + const terminateNode = createTerminateNode(ROOT_NAMESPACE, { x: 0, y: 0 }); + + const edges = [ + createDefaultEdge(forkCloneNode.id, terminateNode.id), + createDefaultEdge(forkCloneNode.id, terminateNode.id), + ]; + const nodeManager = new NodeManager([forkCloneNode, terminateNode]); + + { + const newEdge = createDefaultEdge(forkCloneNode.id, terminateNode.id); + const result = validateEdgeSimple(newEdge, nodeManager, edges); + expect(result.valid).toBe(true); + } + }); + + test('"fork_result" operation only allows 2 outputs', () => { + const forkResultNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { + type: 'fork_result', + ok: { builtin: 'dispose' }, + err: { builtin: 'dispose' }, + }, + 'test_fork_result', + ); + const terminateNode = createTerminateNode(ROOT_NAMESPACE, { x: 0, y: 0 }); + const nodeManager = new NodeManager([forkResultNode, terminateNode]); + + { + const existingEdges = [ + createForkResultOkEdge(forkResultNode.id, terminateNode.id), + ]; + const newEdge = createForkResultErrEdge( + forkResultNode.id, + terminateNode.id, + ); + const result = validateEdgeSimple(newEdge, nodeManager, existingEdges); + expect(result.valid).toBe(true); + } + + { + const existingEdges = [ + createForkResultOkEdge(forkResultNode.id, terminateNode.id), + createForkResultErrEdge(forkResultNode.id, terminateNode.id), + ]; + const newEdge = createForkResultErrEdge( + forkResultNode.id, + terminateNode.id, + ); + const result = validateEdgeSimple(newEdge, nodeManager, existingEdges); + expect(result.valid).toBe(false); + } + }); + + test('buffer edges connecting to a section must have "sectionBuffer" input', () => { + const bufferNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { + type: 'buffer', + }, + 'test_op_buffer', + ); + const sectionNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'section', builder: 'test_section' }, + 'test_op_section', + ); + const nodeManager = new NodeManager([bufferNode, sectionNode]); + + { + const edge = createBufferEdge(bufferNode.id, sectionNode.id, { + type: 'bufferSeq', + seq: 0, + }); + const result = validateEdgeSimple(edge, nodeManager, []); + expect(result.valid).toBe(false); + } + { + const edge = createBufferEdge(bufferNode.id, sectionNode.id, { + type: 'sectionBuffer', + inputId: 'test', + }); + const result = validateEdgeSimple(edge, nodeManager, []); + expect(result.valid).toBe(true); + } + }); + + test('data edges connecting to a section must have "sectionInput" input', () => { + const nodeNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { + type: 'node', + builder: 'test_builder', + next: { builtin: 'dispose' }, + }, + 'test_op_node', + ); + const sectionNode = createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'section', builder: 'test_section' }, + 'test_op_section', + ); + + { + const nodeManager = new NodeManager([nodeNode, sectionNode]); + const edges: DiagramEditorEdge[] = []; + const edge = createDefaultEdge(nodeNode.id, sectionNode.id); + const result = validateEdgeSimple(edge, nodeManager, edges); + expect(result.valid).toBe(false); + } + { + const nodeManager = new NodeManager([nodeNode, sectionNode]); + const edges: DiagramEditorEdge[] = []; + const edge = createDefaultEdge(nodeNode.id, sectionNode.id, { + type: 'sectionInput', + inputId: 'test', + }); + const result = validateEdgeSimple(edge, nodeManager, edges); + expect(result.valid).toBe(true); + } + }); +}); diff --git a/diagram-editor/frontend/utils/connection.ts b/diagram-editor/frontend/utils/connection.ts new file mode 100644 index 00000000..4166ba95 --- /dev/null +++ b/diagram-editor/frontend/utils/connection.ts @@ -0,0 +1,263 @@ +import { + type DiagramEditorEdge, + EDGE_CATEGORIES, + EdgeCategory, + type EdgeTypes, +} from '../edges'; +import type { HandleId } from '../handles'; +import type { NodeManager } from '../node-manager'; +import type { DiagramEditorNode, NodeTypes } from '../nodes'; +import { exhaustiveCheck } from './exhaustive-check'; + +const ALLOWED_OUTPUT_EDGES: Record = { + buffer: ['buffer'], + buffer_access: ['default'], + fork_clone: ['default'], + fork_result: ['forkResultOk', 'forkResultErr'], + join: ['default'], + listen: ['default'], + node: ['default', 'streamOut'], + scope: ['default', 'streamOut'], + section: ['section'], + sectionInput: ['default'], + sectionOutput: [], + sectionBuffer: ['buffer'], + serialized_join: ['default'], + split: ['splitKey', 'splitSeq', 'splitRemaining'], + start: ['default'], + stream_out: [], + terminate: [], + transform: ['default'], + unzip: ['unzip'], +}; + +const ALLOWED_INPUT_EDGE_CATEGORIES: Record = { + buffer: [EdgeCategory.Data], + buffer_access: [EdgeCategory.Data, EdgeCategory.Buffer], + fork_clone: [EdgeCategory.Data], + fork_result: [EdgeCategory.Data], + join: [EdgeCategory.Buffer], + listen: [EdgeCategory.Buffer], + node: [EdgeCategory.Data], + scope: [EdgeCategory.Data], + section: [EdgeCategory.Data, EdgeCategory.Buffer], + sectionInput: [], + sectionOutput: [EdgeCategory.Data], + sectionBuffer: [], + serialized_join: [EdgeCategory.Buffer], + split: [EdgeCategory.Data], + start: [], + stream_out: [EdgeCategory.Stream], + terminate: [EdgeCategory.Data], + transform: [EdgeCategory.Data], + unzip: [EdgeCategory.Data], +}; + +export function getValidEdgeTypes( + sourceNode: DiagramEditorNode, + sourceHandle: HandleId, + targetNode: DiagramEditorNode, + targetHandle: HandleId, +): EdgeTypes[] { + if (sourceHandle !== targetHandle) { + return []; + } + + if (sourceHandle === 'stream' || targetHandle === 'stream') { + return ['streamOut']; + } + + const allowedOutputEdges = [...ALLOWED_OUTPUT_EDGES[sourceNode.type]]; + const allowedInputEdgeCategories = + ALLOWED_INPUT_EDGE_CATEGORIES[targetNode.type]; + return allowedOutputEdges.filter((edgeType) => + allowedInputEdgeCategories.includes(EDGE_CATEGORIES[edgeType]), + ); +} + +enum CardinalityType { + Single, + Pair, + Many, +} + +function getOutputCardinality( + type: NodeTypes, + handleId: HandleId, +): CardinalityType { + if (handleId === 'stream') { + return CardinalityType.Many; + } + + switch (type) { + case 'fork_clone': + case 'unzip': + case 'buffer': + case 'section': + case 'split': { + return CardinalityType.Many; + } + case 'fork_result': { + return CardinalityType.Pair; + } + case 'node': + case 'buffer_access': + case 'join': + case 'serialized_join': + case 'listen': + case 'scope': + case 'stream_out': + case 'transform': + case 'start': + case 'terminate': + case 'sectionBuffer': + case 'sectionInput': + case 'sectionOutput': { + return CardinalityType.Single; + } + default: { + exhaustiveCheck(type); + throw new Error('unknown op type'); + } + } +} + +export type EdgeValidationResult = + | { valid: true; validEdgeTypes: EdgeTypes[] } + | { valid: false; error: string }; + +function createValidationError(error: string): EdgeValidationResult { + return { valid: false, error }; +} + +/** + * Perform a quick check if an edge is valid. + * This only checks if the edge type is valid, does not check for conflicting edges, data correctness etc. + * + * Complexity is O(1). + */ +export function validateEdgeQuick( + edge: DiagramEditorEdge, + nodeManager: NodeManager, +): EdgeValidationResult { + const sourceNode = nodeManager.tryGetNode(edge.source); + const targetNode = nodeManager.tryGetNode(edge.target); + + if (!sourceNode || !targetNode) { + return createValidationError('cannot find source or target node'); + } + + const validEdgeTypes = getValidEdgeTypes( + sourceNode, + edge.sourceHandle, + targetNode, + edge.targetHandle, + ); + if (!validEdgeTypes.includes(edge.type)) { + return createValidationError('invalid edge type'); + } + + return { valid: true, validEdgeTypes }; +} + +/** + * Perform a simple check of the validity of edges. + * Includes the checks in `validateEdgeQuick` and the following: + * * Check that the number of output edges does not exceed what the node allows. + * * Note that it does not check for conflicting edges, e.g. a `fork_result` with 2 "ok" edges is still valid. + * + * Complexity is O(numOfEdges). + */ +export function validateEdgeSimple( + edge: DiagramEditorEdge, + nodeManager: NodeManager, + edges: DiagramEditorEdge[], +): EdgeValidationResult { + const quickCheck = validateEdgeQuick(edge, nodeManager); + if (!quickCheck.valid) { + return quickCheck; + } + + const sourceNode = nodeManager.tryGetNode(edge.source); + const targetNode = nodeManager.tryGetNode(edge.target); + if (!sourceNode || !targetNode) { + return createValidationError('cannot find source or target node'); + } + + if (targetNode.type === 'section') { + if ( + EDGE_CATEGORIES[edge.type] === EdgeCategory.Buffer && + edge.data.input.type !== 'sectionBuffer' + ) { + return createValidationError( + 'target is a section but there is no input slot', + ); + } else if ( + EDGE_CATEGORIES[edge.type] === EdgeCategory.Data && + edge.data.input.type !== 'sectionInput' + ) { + return createValidationError( + 'target is a section but there is no input slot', + ); + } + } + + // Check if the source supports emitting multiple outputs. + // NOTE: All nodes supports "Many" inputs so we don't need to check that. + const outputCardinality = getOutputCardinality( + sourceNode.type, + edge.sourceHandle, + ); + switch (outputCardinality) { + case CardinalityType.Single: { + if (edges.some((e) => e.source === sourceNode.id && edge.id !== e.id)) { + return createValidationError('source node already has an edge'); + } + break; + } + case CardinalityType.Pair: { + let count = 0; + for (const e of edges) { + if (e.source === sourceNode.id && edge.id !== e.id) { + count++; + } + if (count > 1) { + return createValidationError('source node already has two edges'); + } + } + break; + } + case CardinalityType.Many: { + break; + } + default: { + exhaustiveCheck(outputCardinality); + throw new Error('unknown output cardinality'); + } + } + + return { valid: true, validEdgeTypes: quickCheck.validEdgeTypes }; +} + +/** + * Perform a full check of the validity of edges. + * Includes the checks in `validateEdgesSimple` and the following: + * * TODO: Export and send the diagram to `bevy_impulse` for complete validation. + * + * This can be slow so it is not recommended to call this frequently. + */ +export async function validateEdgeFull( + edge: DiagramEditorEdge, + nodeManager: NodeManager, + edges: DiagramEditorEdge[], +): Promise { + const simpleCheck = validateEdgeSimple(edge, nodeManager, edges); + if (!simpleCheck.valid) { + return simpleCheck; + } + + // TODO: Writing the same logic as `bevy_impulse` to do complete validation is hard, it is + // be better to introduce a validation endpoint and have `bevy_impulse` do the validation. + + return { valid: true, validEdgeTypes: simpleCheck.validEdgeTypes }; +} diff --git a/diagram-editor/frontend/utils/exhaustive-check.ts b/diagram-editor/frontend/utils/exhaustive-check.ts new file mode 100644 index 00000000..6ad8cb8b --- /dev/null +++ b/diagram-editor/frontend/utils/exhaustive-check.ts @@ -0,0 +1,19 @@ +/** + * A trick to make typescript checks that the `switch` statement covers all cases. + * + * This works by taking an argument of type `never`, which will only be true when all cases are covered. + * + * # Example + * + * ```ts + * switch (value) { + * case 'valueA': + * break; + * case 'valueB': + * break; + * default: + * exhaustiveCheck(type) + * } + * ``` + */ +export function exhaustiveCheck(_: never) {} diff --git a/diagram-editor/frontend/utils/export-diagram.test.ts b/diagram-editor/frontend/utils/export-diagram.test.ts new file mode 100644 index 00000000..c4b29ef4 --- /dev/null +++ b/diagram-editor/frontend/utils/export-diagram.test.ts @@ -0,0 +1,116 @@ +import { v4 as uuidv4 } from 'uuid'; +import type { DiagramEditorEdge } from '../edges'; +import { NodeManager } from '../node-manager'; +import { + createOperationNode, + createSectionBufferNode, + createSectionInputNode, + createSectionOutputNode, + type DiagramEditorNode, + START_ID, +} from '../nodes'; +import { exportDiagram, exportTemplate } from './export-diagram'; +import { loadDiagramJson } from './load-diagram'; +import { joinNamespaces, ROOT_NAMESPACE } from './namespace'; +import testDiagram from './test-data/test-diagram.json'; +import testDiagramScope from './test-data/test-diagram-scope.json'; + +test('export diagram', () => { + const [_diagram, { nodes, edges }] = loadDiagramJson( + JSON.stringify(testDiagram), + ); + const diagram = exportDiagram(new NodeManager(nodes), edges, {}); + expect(diagram).toEqual(testDiagram); +}); + +test('export diagram with scope', () => { + const [_diagram, { nodes, edges }] = loadDiagramJson( + JSON.stringify(testDiagramScope), + ); + let diagram = exportDiagram(new NodeManager(nodes), edges, {}); + expect(diagram).toEqual(testDiagramScope); + + const nodeManager = new NodeManager(nodes); + const scopeStartNode = nodeManager.getNodeFromNamespaceOpId( + joinNamespaces(ROOT_NAMESPACE, 'scope'), + START_ID, + ); + if (!scopeStartNode) { + fail('scope start node not found'); + } + const scopeStartEdge = edges.find( + (edge) => edge.source === scopeStartNode.id, + ); + if (!scopeStartEdge) { + fail('scope start edge not found'); + } + + scopeStartEdge.target = nodeManager.getNodeFromNamespaceOpId( + joinNamespaces(ROOT_NAMESPACE, 'scope'), + 'mul4', + ).id; + diagram = exportDiagram(nodeManager, edges, {}); + expect(diagram.ops.scope.start).toBe('mul4'); +}); + +test('export diagram with templates', () => { + const nodes: DiagramEditorNode[] = [ + createSectionInputNode('test_input', { builti: 'dispose' }, { x: 0, y: 0 }), + createSectionOutputNode('test_output', { x: 0, y: 0 }), + createSectionBufferNode( + 'test_buffer', + { builtin: 'dispose' }, + { x: 0, y: 0 }, + ), + createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'node', builder: 'test_builder', next: { builtin: 'dispose' } }, + 'test_op_node', + ), + createOperationNode( + ROOT_NAMESPACE, + undefined, + { x: 0, y: 0 }, + { type: 'buffer' }, + 'test_op_buffer', + ), + ]; + const edges: DiagramEditorEdge[] = [ + { + id: uuidv4(), + type: 'default', + source: nodes[0].id, + target: nodes[3].id, + data: { output: {}, input: { type: 'default' } }, + }, + { + id: uuidv4(), + type: 'default', + source: nodes[2].id, + target: nodes[4].id, + data: { output: {}, input: { type: 'default' } }, + }, + { + id: uuidv4(), + type: 'default', + source: nodes[3].id, + target: nodes[1].id, + data: { output: {}, input: { type: 'default' } }, + }, + ]; + const template = exportTemplate(new NodeManager(nodes), edges); + + if (typeof template.inputs !== 'object' || Array.isArray(template.inputs)) { + throw new Error('expected template inputs to be a mapping'); + } + expect(template.inputs.test_input).toBe('test_op_node'); + + expect(template.outputs?.[0]).toBe('test_output'); + + if (typeof template.buffers !== 'object' || Array.isArray(template.buffers)) { + throw new Error('expected template buffers to be a mapping'); + } + expect(template.buffers.test_buffer).toBe('test_op_buffer'); +}); diff --git a/diagram-editor/frontend/utils/export-diagram.ts b/diagram-editor/frontend/utils/export-diagram.ts new file mode 100644 index 00000000..baaadd29 --- /dev/null +++ b/diagram-editor/frontend/utils/export-diagram.ts @@ -0,0 +1,474 @@ +import equal from 'fast-deep-equal'; +import type { DiagramEditorEdge, StreamOutEdge } from '../edges'; +import type { NodeManager } from '../node-manager'; +import { + type DiagramEditorNode, + isBuiltinNode, + isOperationNode, + isScopeNode, +} from '../nodes'; +import type { + BufferSelection, + Diagram, + DiagramOperation, + NextOperation, + SectionTemplate, +} from '../types/api'; +import { exhaustiveCheck } from './exhaustive-check'; +import { ROOT_NAMESPACE, splitNamespaces } from './namespace'; +import { isArrayBufferSelection, isKeyedBufferSelection } from './operation'; + +interface SubOperations { + start: NextOperation; + ops: Record; +} + +function getSubOperations( + root: SubOperations, + namespace: string, +): SubOperations { + const namespaces = splitNamespaces(namespace); + let subOps: SubOperations = root; + for (const namespace of namespaces.slice(1)) { + const scopeOp = subOps.ops[namespace]; + if (!scopeOp || scopeOp.type !== 'scope') { + throw new Error(`expected ${namespace} to be a scope operation`); + } + subOps = scopeOp; + } + return subOps; +} + +function syncStreamOut( + nodeManager: NodeManager, + sourceOp: Extract, + edge: StreamOutEdge, +) { + sourceOp.stream_out = sourceOp.stream_out ? sourceOp.stream_out : {}; + sourceOp.stream_out[edge.data.output.streamId] = + nodeManager.getTargetNextOp(edge); +} + +function getBufferSelection(targetOp: DiagramOperation): BufferSelection { + switch (targetOp.type) { + case 'buffer_access': + case 'listen': + case 'join': + case 'serialized_join': { + return targetOp.buffers; + } + default: { + throw new Error(`"${targetOp.type}" operation does not accept a buffer`); + } + } +} + +function setBufferSelection( + targetOp: DiagramOperation, + bufferSelection: BufferSelection, +): void { + switch (targetOp.type) { + case 'buffer_access': + case 'listen': + case 'join': + case 'serialized_join': { + targetOp.buffers = bufferSelection; + break; + } + default: { + throw new Error(`"${targetOp.type}" operation does not accept a buffer`); + } + } +} + +function syncBufferSelection( + nodeManager: NodeManager, + edge: DiagramEditorEdge, +) { + if (edge.type === 'buffer') { + const targetNode = nodeManager.getNode(edge.target); + if (!isOperationNode(targetNode)) { + throw new Error('expected operation node'); + } + const targetOp = targetNode.data.op; + if (!targetOp) { + throw new Error(`target operation "${edge.target}" not found`); + } + let bufferSelection = getBufferSelection(targetOp); + + if ( + edge.data.input?.type === 'bufferKey' && + Array.isArray(bufferSelection) && + bufferSelection.length === 0 + ) { + // the array is empty so it is safe to change it to a keyed buffer selection + bufferSelection = {}; + setBufferSelection(targetOp, bufferSelection); + } else if ( + edge.data.input?.type === 'bufferSeq' && + typeof bufferSelection === 'object' && + !Array.isArray(bufferSelection) && + Object.keys(bufferSelection).length === 0 + ) { + // the dict is empty so it is safe to change it to an array of buffers + bufferSelection = []; + setBufferSelection(targetOp, bufferSelection); + } + + const sourceNode = nodeManager.getNode(edge.source); + if (sourceNode.type !== 'buffer') { + throw new Error('expected source to be a buffer node'); + } + // check that the buffer selection is compatible + if (edge.type === 'buffer' && edge.data.input?.type === 'bufferSeq') { + if (!isArrayBufferSelection(bufferSelection)) { + throw new Error( + 'a sequential buffer edge must be assigned to an array of buffers', + ); + } + bufferSelection[edge.data.input.seq] = sourceNode.data.opId; + } + if (edge.type === 'buffer' && edge.data.input?.type === 'bufferKey') { + if (!isKeyedBufferSelection(bufferSelection)) { + throw new Error( + 'a keyed buffer edge must be assigned to a keyed buffer selection', + ); + } + bufferSelection[edge.data.input.key] = sourceNode.data.opId; + } + } +} + +/** + * Update a node's data with the edge, this updates fields like `next` and `buffer` to be + * in sync with the edge data. + */ +function syncEdge( + nodeManager: NodeManager, + root: SubOperations, + edge: DiagramEditorEdge, +): void { + if (edge.type === 'buffer') { + syncBufferSelection(nodeManager, edge); + return; + } + + const sourceNode = nodeManager.getNode(edge.source); + + if (isOperationNode(sourceNode)) { + const sourceOp = sourceNode.data.op; + + switch (sourceOp.type) { + case 'node': { + if (edge.type === 'streamOut') { + syncStreamOut(nodeManager, sourceOp, edge); + } else if (edge.type === 'default') { + sourceOp.next = nodeManager.getTargetNextOp(edge); + } + break; + } + case 'join': + case 'serialized_join': + case 'transform': + case 'buffer_access': + case 'listen': { + if (edge.type !== 'default') { + throw new Error('expected "default" edge'); + } + + sourceOp.next = nodeManager.getTargetNextOp(edge); + break; + } + case 'section': { + if (edge.type !== 'section') { + throw new Error('expected section edge'); + } + + if (!sourceOp.connect) { + sourceOp.connect = {}; + } + sourceOp.connect[edge.data.output.output] = + nodeManager.getTargetNextOp(edge); + break; + } + case 'fork_clone': { + if (edge.type !== 'default') { + throw new Error('expected "default" edge'); + } + + const newNextOp = nodeManager.getTargetNextOp(edge); + if (!sourceOp.next.some((next) => equal(next, newNextOp))) { + sourceOp.next.push(newNextOp); + } + break; + } + case 'unzip': { + if (edge.type !== 'unzip') { + throw new Error('expected "unzip" edge'); + } + sourceOp.next[edge.data.output.seq] = nodeManager.getTargetNextOp(edge); + break; + } + case 'fork_result': { + switch (edge.type) { + case 'forkResultOk': { + sourceOp.ok = nodeManager.getTargetNextOp(edge); + break; + } + case 'forkResultErr': { + sourceOp.err = nodeManager.getTargetNextOp(edge); + break; + } + default: { + throw new Error( + 'fork_result operation must have "ok" or "err" edge', + ); + } + } + break; + } + case 'split': { + switch (edge.type) { + case 'splitKey': { + if (!sourceOp.keyed) { + sourceOp.keyed = {}; + } + sourceOp.keyed[edge.data.output.key] = + nodeManager.getTargetNextOp(edge); + break; + } + case 'splitSeq': { + if (!sourceOp.sequential) { + sourceOp.sequential = []; + } + // this works because js allows non-sequential arrays + sourceOp.sequential[edge.data.output.seq] = + nodeManager.getTargetNextOp(edge); + break; + } + case 'splitRemaining': { + sourceOp.remaining = nodeManager.getTargetNextOp(edge); + break; + } + default: { + throw new Error( + 'split operation must have "SplitKey", "SplitSequential", or "SplitRemaining" edge', + ); + } + } + break; + } + case 'buffer': { + throw new Error('buffer operations cannot have connections'); + } + case 'scope': { + if (edge.type === 'streamOut') { + syncStreamOut(nodeManager, sourceOp, edge); + } else if (edge.type !== 'default') { + throw new Error( + 'scope operation must have default or streamOut edge', + ); + } + sourceOp.next = nodeManager.getTargetNextOp(edge); + break; + } + case 'stream_out': { + break; + } + default: { + exhaustiveCheck(sourceOp); + throw new Error('unknown operation type'); + } + } + } else if (sourceNode.type === 'start') { + const subOps: SubOperations = (() => { + if (!sourceNode.parentId) { + return root; + } + const scopeNode = nodeManager.getNode(sourceNode.parentId); + if (!isScopeNode(scopeNode)) { + throw new Error('expected parent to be a scope node'); + } + return scopeNode.data.op; + })(); + const target = nodeManager.getTargetNextOp(edge); + subOps.start = target; + } else if ( + sourceNode.type === 'sectionInput' || + sourceNode.type === 'sectionBuffer' + ) { + sourceNode.data.targetId = nodeManager.getTargetNextOp(edge); + } +} + +/** + * Update the operation connections from the edges. + * + * @param root only used to update the `start` field, does not actually populate the operations. + */ +function syncEdges( + nodeManager: NodeManager, + root: SubOperations, + edges: DiagramEditorEdge[], +): void { + // first clear all the connections + root.start = { builtin: 'dispose' }; + for (const node of nodeManager.iterNodes()) { + switch (node.type) { + case 'node': { + node.data.op.next = { builtin: 'dispose' }; + delete node.data.op.stream_out; + break; + } + case 'scope': { + node.data.op.next = { builtin: 'dispose' }; + delete node.data.op.stream_out; + node.data.op.start = { builtin: 'dispose' }; + break; + } + case 'fork_clone': + case 'unzip': { + node.data.op.next = []; + break; + } + case 'transform': { + node.data.op.next = { builtin: 'dispose' }; + break; + } + case 'join': + case 'serialized_join': + case 'listen': + case 'buffer_access': { + node.data.op.next = { builtin: 'dispose' }; + node.data.op.buffers = []; + break; + } + case 'section': { + node.data.op.connect = {}; + break; + } + case 'fork_result': { + node.data.op.ok = { builtin: 'dispose' }; + node.data.op.err = { builtin: 'dispose' }; + break; + } + case 'split': { + delete node.data.op.keyed; + delete node.data.op.sequential; + delete node.data.op.remaining; + break; + } + case 'buffer': { + break; + } + case 'stream_out': { + break; + } + case 'sectionInput': + case 'sectionBuffer': { + node.data.targetId = { builtin: 'dispose' }; + break; + } + case 'sectionOutput': + case 'start': + case 'terminate': + break; + default: { + exhaustiveCheck(node); + } + } + } + + for (const edge of edges) { + syncEdge(nodeManager, root, edge); + } +} + +/** + * Split the node's namespace if it has one, else return `[ROOT_NAMESPACE]`. + */ +function splitNodeNamespace(node: DiagramEditorNode): string[] { + if (isBuiltinNode(node) || isOperationNode(node)) { + return splitNamespaces(node.data.namespace); + } + return [ROOT_NAMESPACE]; +} + +export function exportDiagram( + nodeManager: NodeManager, + edges: DiagramEditorEdge[], + templates: Record, +): Diagram { + const diagram: Diagram = { + $schema: + 'https://raw.githubusercontent.com/open-rmf/bevy_impulse/refs/heads/main/diagram.schema.json', + version: '0.1.0', + templates, + start: { builtin: 'dispose' }, + ops: {}, + }; + + syncEdges(nodeManager, diagram, edges); + + // visit the nodes breath first to ensure that the parents exist in the diagram before + // populating the children. + const sortedNodes = Array.from(nodeManager.iterNodes()).sort( + (a, b) => splitNodeNamespace(a).length - splitNodeNamespace(b).length, + ); + for (const node of sortedNodes) { + if (!isOperationNode(node)) { + continue; + } + + const subOps = getSubOperations(diagram, node.data.namespace); + subOps.ops[node.data.opId] = { ...node.data.op }; + if (node.data.op.type === 'scope') { + // do not carry over stale ops from the node data + subOps.ops[node.data.opId].ops = {}; + } + } + + return diagram; +} + +export function exportTemplate( + nodeManager: NodeManager, + edges: DiagramEditorEdge[], +): SectionTemplate { + const fakeRoot: SubOperations = { + start: { builtin: 'dispose' }, + ops: {}, + }; + const template = { + inputs: {} as Record, + outputs: [] as string[], + buffers: {} as Record, + ops: fakeRoot.ops, + } satisfies Required; + + syncEdges(nodeManager, fakeRoot, edges); + + // visit the nodes breath first to ensure that the parents exist in the diagram before + // populating the children. + const sortedNodes = Array.from(nodeManager.iterNodes()).sort( + (a, b) => splitNodeNamespace(a).length - splitNodeNamespace(b).length, + ); + for (const node of sortedNodes) { + if (isOperationNode(node)) { + const subOps = getSubOperations(fakeRoot, node.data.namespace); + subOps.ops[node.data.opId] = { ...node.data.op }; + if (node.data.op.type === 'scope') { + // do not carry over stale ops from the node data + subOps.ops[node.data.opId].ops = {}; + } + } else if (node.type === 'sectionInput') { + template.inputs[node.data.remappedId] = node.data.targetId; + } else if (node.type === 'sectionOutput') { + template.outputs.push(node.data.outputId); + } else if (node.type === 'sectionBuffer') { + template.buffers[node.data.remappedId] = node.data.targetId; + } + } + + return template; +} diff --git a/diagram-editor/frontend/utils/layout.ts b/diagram-editor/frontend/utils/layout.ts new file mode 100644 index 00000000..aa72e496 --- /dev/null +++ b/diagram-editor/frontend/utils/layout.ts @@ -0,0 +1,45 @@ +import type { Rect, XYPosition } from '@xyflow/react'; + +export interface LayoutOptions { + nodeWidth: number; + nodeHeight: number; + scopePadding: { + leftRight: number; + topBottom: number; + }; +} + +export const LAYOUT_OPTIONS: LayoutOptions = { + nodeWidth: 200, + nodeHeight: 50, + scopePadding: { + leftRight: 75, + topBottom: 50, + }, +}; + +export function calculateScopeBounds(childrenPosition: XYPosition[]): Rect { + if (childrenPosition.length === 0) { + return { + x: 0, + y: 0, + width: LAYOUT_OPTIONS.scopePadding.leftRight * 2, + height: LAYOUT_OPTIONS.scopePadding.topBottom * 2, + }; + } + const minX = Math.min(...childrenPosition.map((n) => n.x)); + const maxX = Math.max( + ...childrenPosition.map((n) => n.x + LAYOUT_OPTIONS.nodeWidth), + ); + const minY = Math.min(...childrenPosition.map((n) => n.y)); + const maxY = Math.max( + ...childrenPosition.map((n) => n.y + LAYOUT_OPTIONS.nodeHeight), + ); + + return { + x: minX - LAYOUT_OPTIONS.scopePadding.leftRight, + y: minY - LAYOUT_OPTIONS.scopePadding.topBottom, + width: maxX - minX + LAYOUT_OPTIONS.scopePadding.leftRight * 2, + height: maxY - minY + LAYOUT_OPTIONS.scopePadding.topBottom * 2, + }; +} diff --git a/diagram-editor/frontend/utils/load-diagram.test.ts b/diagram-editor/frontend/utils/load-diagram.test.ts new file mode 100644 index 00000000..2958cf56 --- /dev/null +++ b/diagram-editor/frontend/utils/load-diagram.test.ts @@ -0,0 +1,83 @@ +import { applyNodeChanges } from '@xyflow/react'; +import { NodeManager } from '../node-manager'; +import { type OperationNode, START_ID, TERMINATE_ID } from '../nodes'; +import { autoLayout } from './auto-layout'; +import { LAYOUT_OPTIONS } from './layout'; +import { loadDiagramJson, loadTemplate } from './load-diagram'; +import testDiagram from './test-data/test-diagram.json'; + +test('load diagram json and auto layout', () => { + const [_diagram, graph] = loadDiagramJson(JSON.stringify(testDiagram)); + const nodes = applyNodeChanges( + autoLayout(graph.nodes, graph.edges, LAYOUT_OPTIONS), + graph.nodes, + ); + expect(nodes.length).toBe(8); + expect(graph.edges.length).toBe(8); + const nodeManager = new NodeManager(nodes); + + const start = nodeManager.getNodeFromRootOpId(START_ID); + expect(start).toBeDefined(); + + const forkClone = nodeManager.getNodeFromRootOpId( + 'fork_clone', + ) as OperationNode<'fork_clone'>; + expect(forkClone!.data.op).toMatchObject(testDiagram.ops.fork_clone); + expect(forkClone!.position.x).toBe(start!.position.x); + expect(forkClone!.position.y).toBeGreaterThan(start!.position.y); + + const mul3 = nodeManager.getNodeFromRootOpId('mul3') as OperationNode<'node'>; + expect(mul3!.data.op).toMatchObject(testDiagram.ops.mul3); + expect(mul3!.position.x).toBeLessThan(forkClone!.position.x); + expect(mul3!.position.y).toBeGreaterThan(forkClone!.position.y); + + const mul3Buffer = nodeManager.getNodeFromRootOpId( + 'mul3_buffer', + ) as OperationNode<'buffer'>; + expect(mul3Buffer!.data.op).toMatchObject(testDiagram.ops.mul3_buffer); + expect(mul3Buffer!.position.x).toBe(mul3!.position.x); + expect(mul3Buffer!.position.y).toBeGreaterThan(mul3!.position.y); + + const mul4 = nodeManager.getNodeFromRootOpId('mul4') as OperationNode<'node'>; + expect(mul4!.data.op).toMatchObject(testDiagram.ops.mul4); + expect(mul4!.position.x).toBeGreaterThan(forkClone!.position.x); + expect(mul4!.position.y).toBeGreaterThan(forkClone!.position.y); + + const mul4Buffer = nodeManager.getNodeFromRootOpId( + 'mul4_buffer', + ) as OperationNode<'buffer'>; + expect(mul4Buffer!.data.op).toMatchObject(testDiagram.ops.mul4_buffer); + expect(mul4Buffer!.position.x).toBe(mul4!.position.x); + expect(mul4Buffer!.position.y).toBeGreaterThan(mul4!.position.y); + + const join = nodeManager.getNodeFromRootOpId('join') as OperationNode<'join'>; + expect(join!.data.op).toMatchObject(testDiagram.ops.join); + expect(join!.position.x).toBeGreaterThan(mul3Buffer!.position.x); + expect(join!.position.x).toBeLessThan(mul4Buffer!.position.x); + expect(join!.position.y).toBeGreaterThan(mul4Buffer!.position.y); + + const terminate = nodeManager.getNodeFromRootOpId(TERMINATE_ID); + expect(terminate).toBeDefined(); + expect(terminate!.position.x).toBe(join!.position.x); + expect(terminate!.position.y).toBeGreaterThan(join!.position.y); +}); + +test('load template', () => { + const graph = loadTemplate({ + inputs: { + remapped_input: 'target_input', + }, + outputs: ['output'], + buffers: {}, + ops: { + target_input: { + type: 'node', + builder: 'add', + next: 'output', + }, + }, + }); + + expect(graph.nodes.length).toBe(3); + expect(graph.edges.length).toBe(2); +}); diff --git a/diagram-editor/frontend/utils/load-diagram.ts b/diagram-editor/frontend/utils/load-diagram.ts new file mode 100644 index 00000000..4ad1f1e8 --- /dev/null +++ b/diagram-editor/frontend/utils/load-diagram.ts @@ -0,0 +1,166 @@ +import { createDefaultEdge, type DiagramEditorEdge } from '../edges'; +import { NodeManager } from '../node-manager'; +import { + createOperationNode, + createScopeNode, + createSectionBufferNode, + createSectionInputNode, + createSectionOutputNode, + createStartNode, + createTerminateNode, + type DiagramEditorNode, + isOperationNode, + START_ID, +} from '../nodes'; +import type { Diagram, DiagramOperation, SectionTemplate } from '../types/api'; +import { getSchema } from './ajv'; +import { exportDiagram } from './export-diagram'; +import { joinNamespaces, ROOT_NAMESPACE } from './namespace'; +import { buildEdges, isBuiltin } from './operation'; + +export interface Graph { + nodes: DiagramEditorNode[]; + edges: DiagramEditorEdge[]; +} + +export function loadDiagram(diagram: Diagram): Graph { + const graph = buildGraph(diagram); + return graph; +} + +export function loadDiagramJson(jsonStr: string): [Diagram, Graph] { + const diagram = JSON.parse(jsonStr); + const valid = validate(diagram); + if (!valid) { + const error = validate.errors?.[0]; + if (!error) { + throw 'unknown error'; + } + throw `${error.instancePath} ${error.message}`; + } + + return [diagram, loadDiagram(diagram)]; +} + +export function loadEmpty(): Graph { + return { + nodes: [ + createStartNode(ROOT_NAMESPACE, { x: 0, y: 0 }), + createTerminateNode(ROOT_NAMESPACE, { x: 0, y: 400 }), + ], + edges: [], + }; +} + +function buildGraph(diagram: Diagram, initialGraph?: Graph): Graph { + const graph = initialGraph ?? loadEmpty(); + const nodes = graph.nodes; + + interface State { + parentId?: string; + namespace: string; + opId: string; + op: DiagramOperation; + } + + const stack = [ + ...Object.entries(diagram.ops).map( + ([opId, op]) => + ({ parentId: undefined, namespace: ROOT_NAMESPACE, opId, op }) as State, + ), + ]; + + for (let state = stack.pop(); state !== undefined; state = stack.pop()) { + const { parentId, namespace, opId, op } = state; + + if (op.type === 'scope') { + const scopeNodes = createScopeNode( + namespace, + parentId, + { x: 0, y: 0 }, + op, + opId, + ); + const scopeId = scopeNodes[0].id; + nodes.push(...scopeNodes); + + for (const [innerOpId, innerOp] of Object.entries(op.ops)) { + stack.push({ + parentId: scopeId, + namespace: joinNamespaces(namespace, opId), + opId: innerOpId, + op: innerOp, + }); + } + } else { + nodes.push( + createOperationNode(namespace, parentId, { x: 0, y: 0 }, op, opId), + ); + } + } + + const edges = graph.edges; + const diagramStart = diagram.start; + const startNode = isBuiltin(diagramStart) + ? nodes.find((n) => n.type === diagramStart.builtin) + : nodes.find( + (n) => + isOperationNode(n) && + n.data.namespace === ROOT_NAMESPACE && + n.data.opId === diagramStart, + ); + if (startNode) { + edges.push( + createDefaultEdge(joinNamespaces(ROOT_NAMESPACE, START_ID), startNode.id), + ); + } + edges.push(...buildEdges(graph.nodes)); + + return graph; +} + +const validate = getSchema('Diagram'); + +export function loadTemplate(template: SectionTemplate): Graph { + const stubDiagram = exportDiagram(new NodeManager([]), [], {}); + stubDiagram.ops = template.ops; + const initialNodes: DiagramEditorNode[] = []; + + if (template.inputs) { + if (Array.isArray(template.inputs)) { + for (const input of template.inputs) { + initialNodes.push(createSectionInputNode(input, input, { x: 0, y: 0 })); + } + } else { + for (const [remappedId, targetId] of Object.entries(template.inputs)) { + initialNodes.push( + createSectionInputNode(remappedId, targetId, { x: 0, y: 0 }), + ); + } + } + } + + if (template.buffers) { + if (Array.isArray(template.buffers)) { + for (const buffer of template.buffers) { + initialNodes.push( + createSectionBufferNode(buffer, buffer, { x: 0, y: 0 }), + ); + } + } else { + for (const [remappedId, targetId] of Object.entries(template.buffers)) { + initialNodes.push( + createSectionBufferNode(remappedId, targetId, { x: 0, y: 0 }), + ); + } + } + } + + if (template.outputs) { + for (const output of template.outputs) { + initialNodes.push(createSectionOutputNode(output, { x: 0, y: 0 })); + } + } + + return buildGraph(stubDiagram, { nodes: initialNodes, edges: [] }); +} diff --git a/diagram-editor/frontend/utils/namespace.ts b/diagram-editor/frontend/utils/namespace.ts new file mode 100644 index 00000000..8b377fd6 --- /dev/null +++ b/diagram-editor/frontend/utils/namespace.ts @@ -0,0 +1,13 @@ +/** + * The root namespace is an empty string so namespaces that begins with `:` can be identified as + * absolute. + */ +export const ROOT_NAMESPACE = ''; + +export function joinNamespaces(...namespaces: string[]): string { + return namespaces.join(':'); +} + +export function splitNamespaces(namespace: string): string[] { + return namespace.split(':'); +} diff --git a/diagram-editor/frontend/utils/operation.ts b/diagram-editor/frontend/utils/operation.ts new file mode 100644 index 00000000..a822be5e --- /dev/null +++ b/diagram-editor/frontend/utils/operation.ts @@ -0,0 +1,347 @@ +import { + createBufferEdge, + createDefaultEdge, + createForkResultErrEdge, + createForkResultOkEdge, + createSectionEdge, + createSplitKeyEdge, + createSplitRemainingEdge, + createSplitSeqEdge, + createUnzipEdge, + type DiagramEditorEdge, +} from '../edges'; +import { NodeManager } from '../node-manager'; +import { + type DiagramEditorNode, + isOperationNode, + type OperationNode, + START_ID, +} from '../nodes'; +import type { + BufferSelection, + BuiltinTarget, + DiagramOperation, + NextOperation, +} from '../types/api'; +import { exhaustiveCheck } from './exhaustive-check'; +import { joinNamespaces, ROOT_NAMESPACE } from './namespace'; + +export function isKeyedBufferSelection( + bufferSelection: BufferSelection, +): bufferSelection is Record { + return typeof bufferSelection !== 'string' && !Array.isArray(bufferSelection); +} + +export function isArrayBufferSelection( + bufferSelection: BufferSelection, +): bufferSelection is NextOperation[] { + return Array.isArray(bufferSelection); +} + +function createStreamOutEdges( + streamOuts: Record, + node: OperationNode, + nodeManager: NodeManager, +): DiagramEditorEdge[] { + const edges: DiagramEditorEdge[] = []; + for (const nextOp of Object.values(streamOuts)) { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + nextOp, + )?.id; + if (target) { + edges.push(createDefaultEdge(node.id, target)); + } + } + return edges; +} + +function createBufferEdges( + node: OperationNode, + buffers: BufferSelection, + nodeManager: NodeManager, +): DiagramEditorEdge[] { + const edges: DiagramEditorEdge[] = []; + if (isArrayBufferSelection(buffers)) { + for (const [idx, buffer] of buffers.entries()) { + const source = nodeManager.getNodeFromNextOp( + node.data.namespace, + buffer, + )?.id; + if (source) { + edges.push( + createBufferEdge(source, node.id, { type: 'bufferSeq', seq: idx }), + ); + } + } + } else if (isKeyedBufferSelection(buffers)) { + for (const [key, buffer] of Object.entries(buffers)) { + const source = nodeManager.getNodeFromNextOp( + node.data.namespace, + buffer, + )?.id; + if (source) { + edges.push( + createBufferEdge(source, node.id, { type: 'bufferKey', key }), + ); + } + } + } else { + const source = nodeManager.getNodeFromNextOp( + node.data.namespace, + buffers, + )?.id; + if (source) { + edges.push( + createBufferEdge(source, node.id, { type: 'bufferSeq', seq: 0 }), + ); + } + } + + return edges; +} + +export function buildEdges(nodes: DiagramEditorNode[]): DiagramEditorEdge[] { + const edges: DiagramEditorEdge[] = []; + const nodeManager = new NodeManager(nodes); + + interface State { + namespace: string; + opId: string; + op: DiagramOperation; + } + const stack = [ + ...nodes.map( + (node) => + ({ + namespace: ROOT_NAMESPACE, + opId: node.data.opId, + op: node.data.op, + }) as State, + ), + ]; + + for (const node of nodes) { + if (isOperationNode(node)) { + const op = node.data.op; + const opId = node.data.opId; + + switch (op.type) { + case 'buffer': { + break; + } + case 'buffer_access': + case 'join': + case 'serialized_join': + case 'listen': { + edges.push(...createBufferEdges(node, op.buffers, nodeManager)); + + const nextNodeId = nodeManager.getNodeFromNextOp( + node.data.namespace, + op.next, + )?.id; + if (nextNodeId) { + edges.push(createDefaultEdge(node.id, nextNodeId)); + } + + break; + } + case 'node': { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + op.next, + )?.id; + if (target) { + edges.push(createDefaultEdge(node.id, target)); + } + if (op.stream_out) { + edges.push( + ...createStreamOutEdges(op.stream_out, node, nodeManager), + ); + } + break; + } + case 'transform': { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + op.next, + )?.id; + if (target) { + edges.push(createDefaultEdge(node.id, target)); + } + break; + } + case 'fork_clone': { + for (const next of op.next.values()) { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + next, + )?.id; + if (target) { + edges.push(createDefaultEdge(node.id, target)); + } + } + break; + } + case 'unzip': { + for (const [idx, next] of op.next.entries()) { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + next, + )?.id; + if (target) { + edges.push(createUnzipEdge(node.id, target, { seq: idx })); + } + } + break; + } + case 'fork_result': { + const okTarget = nodeManager.getNodeFromNextOp( + node.data.namespace, + op.ok, + )?.id; + const errTarget = nodeManager.getNodeFromNextOp( + node.data.namespace, + op.err, + )?.id; + if (okTarget) { + edges.push(createForkResultOkEdge(node.id, okTarget)); + } + if (errTarget) { + edges.push(createForkResultErrEdge(node.id, errTarget)); + } + break; + } + case 'split': { + if (op.keyed) { + for (const [key, next] of Object.entries(op.keyed)) { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + next, + )?.id; + if (target) { + edges.push(createSplitKeyEdge(node.id, target, { key })); + } + } + } + if (op.sequential) { + for (const [idx, next] of op.sequential.entries()) { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + next, + )?.id; + if (target) { + edges.push(createSplitSeqEdge(node.id, target, { seq: idx })); + } + } + } + if (op.remaining) { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + op.remaining, + )?.id; + if (target) { + edges.push(createSplitRemainingEdge(node.id, target)); + } + } + break; + } + case 'section': { + if (op.connect) { + for (const [outputId, next] of Object.entries(op.connect)) { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + next, + )?.id; + if (target) { + edges.push( + createSectionEdge(node.id, target, { + output: outputId, + }), + ); + } + } + } + break; + } + case 'scope': { + const target = nodeManager.getNodeFromNextOp( + node.data.namespace, + op.next, + )?.id; + if (target) { + edges.push(createDefaultEdge(node.id, target)); + } + + if (op.stream_out) { + edges.push( + ...createStreamOutEdges(op.stream_out, node, nodeManager), + ); + } + + const scopeStart = nodeManager.getNodeFromNamespaceOpId( + joinNamespaces(node.data.namespace, opId), + START_ID, + ); + const scopeStartTarget = nodeManager.getNodeFromNextOp( + joinNamespaces(node.data.namespace, opId), + op.start, + ); + if (scopeStart && scopeStartTarget) { + edges.push(createDefaultEdge(scopeStart.id, scopeStartTarget.id)); + } + + for (const [innerOpId, innerOp] of Object.entries(op.ops)) { + stack.push({ + namespace: joinNamespaces(node.data.namespace, opId), + opId: innerOpId, + op: innerOp, + }); + } + + break; + } + case 'stream_out': { + break; + } + default: { + exhaustiveCheck(op); + throw new Error('unknown op'); + } + } + } else if (node.type === 'sectionInput' || node.type === 'sectionBuffer') { + const target = nodeManager.getNodeFromNextOp( + ROOT_NAMESPACE, + node.data.targetId, + )?.id; + if (target) { + edges.push(createDefaultEdge(node.id, target)); + } + } + } + + return edges; +} + +export function isBuiltin(next: unknown): next is { builtin: BuiltinTarget } { + return next !== null && typeof next === 'object' && 'builtin' in next; +} + +export function isSectionBuilder( + nodeData: Extract, +): nodeData is Extract & { + builder: string; +} { + return 'builder' in nodeData; +} + +export function formatNextOperation(nextOp: NextOperation): string { + if (isBuiltin(nextOp)) { + return `builtin:${nextOp.builtin}`; + } + if (typeof nextOp === 'object') { + const [ns, opId] = Object.entries(nextOp)[0]; + return `${ns}:${opId}`; + } + return nextOp; +} diff --git a/diagram-editor/frontend/utils/test-data/test-diagram-scope.json b/diagram-editor/frontend/utils/test-data/test-diagram-scope.json new file mode 100644 index 00000000..861b4c1e --- /dev/null +++ b/diagram-editor/frontend/utils/test-data/test-diagram-scope.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://raw.githubusercontent.com/open-rmf/bevy_impulse/refs/heads/main/diagram.schema.json", + "version": "0.1.0", + "templates": {}, + "start": "scope", + "ops": { + "scope": { + "type": "scope", + "start": "mul3", + "ops": { + "mul3": { + "type": "node", + "builder": "mul", + "config": 3, + "next": { "builtin": "terminate" } + }, + "mul4": { + "type": "node", + "builder": "mul", + "config": 4, + "next": { "builtin": "terminate" } + } + }, + "next": "add4" + }, + "add4": { + "type": "node", + "builder": "add", + "config": 4, + "next": { "builtin": "terminate" } + } + } +} diff --git a/diagram-editor/frontend/utils/test-data/test-diagram.json b/diagram-editor/frontend/utils/test-data/test-diagram.json new file mode 100644 index 00000000..9f5c7762 --- /dev/null +++ b/diagram-editor/frontend/utils/test-data/test-diagram.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://raw.githubusercontent.com/open-rmf/bevy_impulse/refs/heads/main/diagram.schema.json", + "version": "0.1.0", + "templates": {}, + "start": "fork_clone", + "ops": { + "fork_clone": { + "type": "fork_clone", + "next": ["mul3", "mul4"] + }, + "mul3": { + "type": "node", + "builder": "mul", + "config": 3, + "next": "mul3_buffer" + }, + "mul3_buffer": { + "type": "buffer" + }, + "mul4": { + "type": "node", + "builder": "mul", + "config": 4, + "next": "mul4_buffer" + }, + "mul4_buffer": { + "type": "buffer" + }, + "join": { + "type": "join", + "buffers": ["mul3_buffer", "mul4_buffer"], + "next": { "builtin": "terminate" } + } + } +} diff --git a/diagram-editor/frontend/utils/unique-value.ts b/diagram-editor/frontend/utils/unique-value.ts new file mode 100644 index 00000000..6553ab18 --- /dev/null +++ b/diagram-editor/frontend/utils/unique-value.ts @@ -0,0 +1,20 @@ +/** + * Returns a string that does not conflict with existing values by adding suffix to it. + */ +export function addUniqueSuffix( + prefix: string, + existing: string[], + sep: string = '_', +): string { + const set = new Set(existing); + if (!set.has(prefix)) { + return prefix; + } + for (let i = 1; i < existing.length; ++i) { + const candidate = `${prefix}${sep}${i}`; + if (!set.has(candidate)) { + return candidate; + } + } + return `${prefix}${sep}${existing.length + 1}`; +} diff --git a/diagram-editor/jest.config.js b/diagram-editor/jest.config.js new file mode 100644 index 00000000..9d30ccf8 --- /dev/null +++ b/diagram-editor/jest.config.js @@ -0,0 +1,11 @@ +import { createDefaultPreset } from 'ts-jest'; + +const tsJestTransformCfg = createDefaultPreset().transform; + +/** @type {import("jest").Config} **/ +export default { + testEnvironment: 'node', + transform: { + ...tsJestTransformCfg, + }, +}; diff --git a/diagram-editor/package.json b/diagram-editor/package.json new file mode 100644 index 00000000..41f1f469 --- /dev/null +++ b/diagram-editor/package.json @@ -0,0 +1,52 @@ +{ + "name": "diagram-editor", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rsbuild build", + "generate-types": "node scripts/generate-types.mjs", + "test": "jest --watch", + "check": "biome check --write", + "check:ts": "tsc --noEmit", + "dev": "rsbuild dev --open", + "dev:backend": "cargo run --manifest-path ../examples/diagram/calculator/Cargo.toml --no-default-features -F api -- serve --port 3001", + "format": "biome format --write", + "preview": "rsbuild preview", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@fontsource/roboto": "^5.2.5", + "@material-symbols/font-400": "^0.31.9", + "@mui/material": "^7.1.0", + "@types/dagre": "^0.7.53", + "@xyflow/react": "^12.6.1", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "cel-js": "^0.7.1", + "dagre": "^0.8.5", + "fast-deep-equal": "^3.1.3", + "fflate": "^0.8.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "rxjs": "^7.8.2", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@rsbuild/core": "^1.3.15", + "@rsbuild/plugin-react": "^1.3.0", + "@storybook/preview-api": "^8.6.14", + "@types/jest": "^29.5.14", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "json-schema-to-typescript": "15.0.4", + "storybook": "^9.0.9", + "storybook-react-rsbuild": "^2.0.1", + "ts-jest": "^29.3.4" + } +} diff --git a/diagram-editor/rsbuild.config.ts b/diagram-editor/rsbuild.config.ts new file mode 100644 index 00000000..7344b1a9 --- /dev/null +++ b/diagram-editor/rsbuild.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from '@rsbuild/core'; +import { pluginReact } from '@rsbuild/plugin-react'; + +export default defineConfig({ + server: { + proxy: { + '/api': 'http://localhost:3001', + }, + }, + source: { + entry: { + index: './frontend/index.tsx', + }, + }, + html: { + title: 'Bevy Impulse Diagram Editor', + meta: { + viewport: 'width=device-width, initial-scale=1.0', + }, + }, + plugins: [pluginReact()], +}); diff --git a/diagram-editor/scripts/generate-types.mjs b/diagram-editor/scripts/generate-types.mjs new file mode 100644 index 00000000..31ef4963 --- /dev/null +++ b/diagram-editor/scripts/generate-types.mjs @@ -0,0 +1,134 @@ +import { execSync } from 'node:child_process'; +import fs, { writeFileSync } from 'node:fs'; +import { compile } from 'json-schema-to-typescript'; + +const MERGE_FIELDS = ['$ref', 'oneOf', 'allOf', 'anyOf']; + +/** + * json schema draft-07 (used by json-schema-to-typescript) does not merge certain fields like `$ref` and `oneOf`. + * Workaround by putting them in a `allOf`. + */ +function moveIntoAllOf(source) { + let needMove = false; + for (const k of MERGE_FIELDS) { + if (k in source) { + needMove = true; + break; + } + } + if (!needMove) { + return; + } + + const allOf = []; + + for (const k of MERGE_FIELDS) { + if (!(k in source)) { + continue; + } + const obj = {}; + obj[k] = source[k]; + delete source[k]; + allOf.push(obj); + } + + const remaining = {}; + for (const k of Object.keys(source)) { + if (k === 'allOf') { + continue; + } + remaining[k] = source[k]; + delete source[k]; + } + if ('type' in remaining) { + allOf.push(remaining); + } + + // don't need "allOf" if there is only one item in it. + if (allOf.length === 1) { + for (const k of Object.keys(allOf[0])) { + source[k] = allOf[0][k]; + } + } else { + source.allOf = allOf; + } +} + +async function generate(name, schema, outputPath, preprocessedOutputPath) { + // preprocess the schema to workaround https://github.com/bcherny/json-schema-to-typescript/issues/637 and https://github.com/bcherny/json-schema-to-typescript/issues/613 + const workingSet = [...Object.values(schema.$defs)]; + while (workingSet.length > 0) { + const schema = workingSet.pop(); + if (typeof schema !== 'object') { + continue; + } + + if ('properties' in schema) { + for (const property of Object.values(schema.properties)) { + workingSet.push(property); + } + } + + if ('oneOf' in schema) { + workingSet.push(...Object.values(schema.oneOf)); + } + + if ('anyOf' in schema) { + workingSet.push(...Object.values(schema.anyOf)); + } + + moveIntoAllOf(schema); + + // json schema draft-07 (used by json-schema-to-typescript) does not merge $ref, workaround + // by putting the $ref and other fields in a `allOf`. + if ('$ref' in schema && Object.keys(schema).length > 1) { + const $ref = schema.$ref; + const copy = { + ...schema, + }; + delete copy.$ref; + + for (const k of Object.keys(schema)) { + delete schema[k]; + } + + if ( + Object.keys(copy).some( + (k) => !['description', 'title', 'default'].includes(k), + ) + ) { + // At least one field is not metadata only field. + const allOf = [copy, { $ref }]; + schema.allOf = allOf; + } else { + // All the remaining fields are metadata only fields. We cannot use `allOf` as the + // metadata only branch will allow any types. + schema.$ref = $ref; + } + } + } + + const output = await compile(schema, name, { unreachableDefinitions: true }); + const fd = fs.openSync(outputPath, 'w'); + fs.writeSync(fd, output); + fs.closeSync(fd); + + writeFileSync(preprocessedOutputPath, JSON.stringify(schema, undefined, 2)); +} + +const apiSchema = execSync( + 'cargo run -p bevy_impulse_diagram_editor -F json_schema --bin print_schema', + { + encoding: 'utf-8', + stdio: 'pipe', + }, +); + +await generate( + 'DiagramEditorApi', + JSON.parse(apiSchema), + 'frontend/types/api.d.ts', + 'frontend/api.preprocessed.schema.json', +); + +execSync('biome format --write frontend/types/api.d.ts'); diff --git a/diagram-editor/server/api/error_responses.rs b/diagram-editor/server/api/error_responses.rs new file mode 100644 index 00000000..6f035208 --- /dev/null +++ b/diagram-editor/server/api/error_responses.rs @@ -0,0 +1,30 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use bevy_impulse::{Cancellation, CancellationCause}; + +pub(super) struct WorkflowCancelledResponse<'a>(pub(super) &'a Cancellation); + +impl<'a> IntoResponse for WorkflowCancelledResponse<'a> { + fn into_response(self) -> Response { + let msg = match *self.0.cause { + CancellationCause::TargetDropped(_) => "target dropped", + CancellationCause::Unreachable(_) => "unreachable", + CancellationCause::Filtered(_) => "filtered", + CancellationCause::Triggered(_) => "triggered", + CancellationCause::Supplanted(_) => "supplanted", + CancellationCause::InvalidSpan(_) => "invalid span", + CancellationCause::CircularCollect(_) => "circular collect", + CancellationCause::Undeliverable => "undeliverable", + CancellationCause::PoisonedMutexInPromise => "poisoned mutex in promise", + CancellationCause::Broken(_) => "broken", + }; + Response::builder() + .status(StatusCode::UNPROCESSABLE_ENTITY) + .body(format!("workflow cancelled: {}", msg)) + .map_or(StatusCode::INTERNAL_SERVER_ERROR.into_response(), |resp| { + resp.into_response() + }) + } +} diff --git a/diagram-editor/server/api/executor.rs b/diagram-editor/server/api/executor.rs new file mode 100644 index 00000000..98aabdaa --- /dev/null +++ b/diagram-editor/server/api/executor.rs @@ -0,0 +1,598 @@ +use crate::api::error_responses::WorkflowCancelledResponse; + +use super::websocket::{WebsocketSinkExt, WebsocketStreamExt}; +use axum::{ + extract::{ + ws::{self}, + State, + }, + http::StatusCode, + response::{self, Response}, + routing::{self, post}, + Json, Router, +}; +use bevy_impulse::{trace, Diagram, DiagramElementRegistry, OperationStarted, Promise, RequestExt}; +use futures_util::StreamExt; +use serde::{Deserialize, Serialize}; +use std::{ + error::Error, + sync::{Arc, Mutex}, + time::Duration, +}; +use tokio::sync::mpsc::error::TryRecvError; +use tracing::{error, warn}; + +type BroadcastRecvError = tokio::sync::broadcast::error::RecvError; + +type WorkflowResponseResult = Result, Box>; +type WorkflowResponseSender = tokio::sync::oneshot::Sender; + +type WorkflowFeedback = OperationStarted; + +#[derive(bevy_ecs::component::Component)] +struct FeedbackSender(tokio::sync::broadcast::Sender); + +struct Context { + diagram: Diagram, + request: serde_json::Value, + registry: Arc>, + response_tx: WorkflowResponseSender, + feedback_tx: Option, +} + +#[derive(Clone)] +struct ExecutorState { + registry: Arc>, + send_chan: tokio::sync::mpsc::Sender, + response_timeout: Duration, +} + +#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] +#[cfg_attr(test, derive(serde::Serialize))] +#[derive(Deserialize)] +pub struct PostRunRequest { + diagram: Diagram, + request: serde_json::Value, +} + +/// Sends a request to the executor system and wait for the response. +async fn post_run( + state: State, + Json(body): Json, +) -> response::Result> { + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + if let Err(err) = state + .send_chan + .send(Context { + registry: state.registry.clone(), + diagram: body.diagram, + request: body.request, + response_tx, + feedback_tx: None, + }) + .await + { + error!("{}", err); + return Err(StatusCode::INTERNAL_SERVER_ERROR.into()); + } + + let response = tokio::time::timeout( + state.response_timeout, + (async || -> response::Result { + let workflow_response = match response_rx.await { + Ok(response) => response, + Err(err) => { + error!("{}", err); + return Err(StatusCode::INTERNAL_SERVER_ERROR.into()); + } + }; + + match workflow_response { + Ok(promise) => { + let promise_state = promise.await; + if promise_state.is_available() { + if let Some(result) = promise_state.available() { + Ok(result) + } else { + Err(StatusCode::INTERNAL_SERVER_ERROR.into()) + } + } else if promise_state.is_cancelled() { + if let Some(cancellation) = promise_state.cancellation() { + Err(WorkflowCancelledResponse(cancellation).into()) + } else { + Err(StatusCode::INTERNAL_SERVER_ERROR.into()) + } + } else { + Err(StatusCode::INTERNAL_SERVER_ERROR.into()) + } + } + Err(err) => Err(Response::builder() + .status(StatusCode::UNPROCESSABLE_ENTITY) + .body(err.to_string()) + .map_or(StatusCode::INTERNAL_SERVER_ERROR.into(), |resp| resp.into())), + } + })(), + ) + .await + .map_err(|_| StatusCode::REQUEST_TIMEOUT)??; + + Ok(Json(response)) +} + +#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] +#[cfg_attr(test, derive(serde::Deserialize))] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub enum DebugSessionEnd { + Ok(serde_json::Value), + Err(String), +} + +impl DebugSessionEnd { + fn err_from_status_code(status_code: StatusCode) -> Self { + Self::Err(status_code.to_string()) + } +} + +#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] +#[cfg_attr(test, derive(serde::Deserialize))] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub enum DebugSessionFeedback { + OperationStarted(String), +} + +#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] +#[cfg_attr(test, derive(serde::Deserialize))] +#[derive(Serialize)] +#[serde(rename_all = "camelCase", tag = "type")] +pub enum DebugSessionMessage { + Feedback(DebugSessionFeedback), + Finish(DebugSessionEnd), +} + +/// Start a debug session. +async fn ws_debug(mut write: W, mut read: R, state: State) +where + W: WebsocketSinkExt, + R: WebsocketStreamExt, +{ + let req: PostRunRequest = if let Some(req) = read.next_json().await { + req + } else { + return; + }; + + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + let (feedback_tx, mut feedback_rx) = tokio::sync::broadcast::channel(10); + if let Err(err) = state + .send_chan + .send(Context { + registry: state.registry.clone(), + diagram: req.diagram, + request: req.request, + response_tx, + feedback_tx: Some(FeedbackSender(feedback_tx)), + }) + .await + { + error!("{}", err); + write + .send_json(&DebugSessionMessage::Finish( + DebugSessionEnd::err_from_status_code(StatusCode::INTERNAL_SERVER_ERROR), + )) + .await; + return; + } + + let write = tokio::sync::Mutex::new(write); + + let process_response = async || { + let response_result = response_rx.await; + + let workflow_response = match response_result { + Ok(response) => response, + Err(err) => { + error!("{}", err); + write + .lock() + .await + .send_json(&DebugSessionMessage::Finish( + DebugSessionEnd::err_from_status_code(StatusCode::INTERNAL_SERVER_ERROR), + )) + .await; + return; + } + }; + + match workflow_response { + // type annotations needed on `promise.await` + Ok(promise) => match promise.await.available() { + Some(result) => { + write + .lock() + .await + .send_json(&DebugSessionMessage::Finish(DebugSessionEnd::Ok(result))) + .await; + } + None => { + write + .lock() + .await + .send_json(&DebugSessionMessage::Finish( + DebugSessionEnd::err_from_status_code( + StatusCode::INTERNAL_SERVER_ERROR, + ), + )) + .await; + return; + } + }, + Err(err) => { + write + .lock() + .await + .send_json(&DebugSessionMessage::Finish(DebugSessionEnd::Err( + err.to_string(), + ))) + .await; + return; + } + }; + }; + + let mut process_feedback = async || loop { + let feedback = feedback_rx.recv().await; + + match feedback { + Ok(feedback) => { + let op_id = if let Some(id) = feedback.info.id() { + id.to_string() + } else { + "[unknown]".to_string() + }; + + write + .lock() + .await + .send_json(&DebugSessionMessage::Feedback( + DebugSessionFeedback::OperationStarted(op_id), + )) + .await; + } + Err(e) => match e { + BroadcastRecvError::Closed => { + break; + } + BroadcastRecvError::Lagged(_) => { + warn!("{}", e); + break; + } + }, + } + }; + + tokio::select! { + _ = process_response() => {}, + _ = process_feedback() => {}, + }; +} + +#[derive(bevy_ecs::system::Resource)] +struct RequestReceiver(tokio::sync::mpsc::Receiver); + +/// Receives a request from executor service and schedules the workflow. +fn execute_requests( + mut rx: bevy_ecs::system::ResMut, + mut cmds: bevy_ecs::system::Commands, + mut app_exit_events: bevy_ecs::event::EventWriter, +) { + let rx = &mut rx.0; + match rx.try_recv() { + Ok(ctx) => { + let registry = &*ctx.registry.lock().unwrap(); + let maybe_promise = match ctx.diagram.spawn_io_workflow(&mut cmds, registry) { + Ok(workflow) => { + if let Some(feedback_tx) = ctx.feedback_tx { + // FIXME: the provider id is different from the session id, there doesn't seem to be possible to get the session id + cmds.entity(workflow.provider()).insert(feedback_tx); + } + let promise: Promise = + cmds.request(ctx.request, workflow).take_response(); + Ok(promise) + } + Err(err) => Err(err.into()), + }; + // assuming that workflows are automatically cancelled when the promise is dropped. + if let Err(_) = ctx.response_tx.send(maybe_promise) { + error!("failed to send response") + } + } + Err(err) => match err { + TryRecvError::Empty => {} + TryRecvError::Disconnected => { + app_exit_events.send_default(); + } + }, + } +} + +fn debug_feedback( + mut op_started: bevy_ecs::event::EventReader, + feedback_query: bevy_ecs::system::Query<&FeedbackSender>, +) { + for ev in op_started.read() { + let session = match ev.session_stack.last() { + Some(session) => session, + None => { + continue; + } + }; + match feedback_query.get(*session) { + Ok(feedback_tx) => { + if let Err(e) = feedback_tx.0.send(ev.clone()) { + error!("{}", e); + } + } + Err(_) => { + // the session has no feedback channel + } + } + } +} + +#[non_exhaustive] +pub struct ExecutorOptions { + pub response_timeout: Duration, +} + +impl Default for ExecutorOptions { + fn default() -> Self { + Self { + response_timeout: Duration::from_secs(15), + } + } +} + +fn setup_bevy_app( + app: &mut bevy_app::App, + registry: DiagramElementRegistry, + options: &ExecutorOptions, +) -> ExecutorState { + let (request_tx, request_rx) = tokio::sync::mpsc::channel::(10); + app.insert_resource(RequestReceiver(request_rx)); + app.add_systems(bevy_app::Update, execute_requests); + app.add_systems(bevy_app::Update, debug_feedback); + ExecutorState { + registry: Arc::new(Mutex::new(registry)), + send_chan: request_tx, + response_timeout: options.response_timeout, + } +} + +pub(super) fn new_router( + app: &mut bevy_app::App, + registry: DiagramElementRegistry, + options: ExecutorOptions, +) -> Router { + let executor_state = setup_bevy_app(app, registry, &options); + + Router::new() + .route("/run", post(post_run)) + .route( + "/debug", + routing::any( + async |ws: ws::WebSocketUpgrade, state: State| { + ws.on_upgrade(|socket| { + let (write, read) = socket.split(); + ws_debug(write, read, state) + }) + }, + ), + ) + .with_state(executor_state) +} + +#[cfg(test)] +mod tests { + use std::thread; + + use super::*; + use axum::{ + body, + http::{header, Request}, + }; + use bevy_impulse::{ImpulseAppPlugin, NodeBuilderOptions}; + use futures_util::SinkExt; + use mime_guess::mime; + use serde_json::json; + use tower::ServiceExt; + + struct TestFixture { + router: Router, + cleanup_test: CleanupFn, + } + + fn setup_test() -> TestFixture { + let mut registry = DiagramElementRegistry::new(); + registry.register_node_builder(NodeBuilderOptions::new("add7"), |builder, _config: ()| { + builder.create_map_block(|req: i32| req + 7) + }); + + let mut app = bevy_app::App::new(); + app.add_plugins(ImpulseAppPlugin::default()); + let (send_stop, mut recv_stop) = tokio::sync::oneshot::channel::<()>(); + app.add_systems( + bevy_app::Update, + move |mut app_exit: bevy_ecs::event::EventWriter| { + if let Ok(_) = recv_stop.try_recv() { + app_exit.send_default(); + } + }, + ); + + let router = new_router(&mut app, registry, ExecutorOptions::default()); + let join_handle = thread::spawn(move || { + app.run(); + }); + + TestFixture { + router, + cleanup_test: move || { + send_stop.send(()).unwrap(); + join_handle.join().unwrap(); + }, + } + } + + fn new_add7_diagram() -> Diagram { + Diagram::from_json(json!({ + "version": "0.1.0", + "start": "add7", + "ops": { + "add7": { + "type": "node", + "builder": "add7", + "next": { "builtin": "terminate" }, + }, + }, + })) + .unwrap() + } + + #[tokio::test] + #[test_log::test] + async fn test_post_run() { + let TestFixture { + router, + cleanup_test, + } = setup_test(); + + let diagram = new_add7_diagram(); + + let request_body = PostRunRequest { + diagram, + request: serde_json::Value::from(5), + }; + let response = router + .oneshot( + Request::post("/run") + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .body(serde_json::to_string(&request_body).unwrap()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap(), + mime::APPLICATION_JSON + ); + let resp_bytes = body::to_bytes(response.into_body(), 1024 * 1024) + .await + .unwrap(); + let resp_str = str::from_utf8(&resp_bytes).unwrap(); + let resp: i32 = serde_json::from_str(resp_str).unwrap(); + assert_eq!(resp, 12); + + cleanup_test(); + } + + struct WsTestFixture { + executor_state: ExecutorState, + cleanup_test: CleanupFn, + } + + fn setup_ws_test() -> WsTestFixture { + let mut app = bevy_app::App::new(); + app.add_plugins(ImpulseAppPlugin::default()); + let (send_stop, mut recv_stop) = tokio::sync::oneshot::channel::<()>(); + app.add_systems( + bevy_app::Update, + move |mut app_exit: bevy_ecs::event::EventWriter| { + if let Ok(_) = recv_stop.try_recv() { + app_exit.send_default(); + } + }, + ); + + let mut registry = DiagramElementRegistry::new(); + registry.register_node_builder(NodeBuilderOptions::new("add7"), |builder, _config: ()| { + builder.create_map_block(|req: i32| req + 7) + }); + let executor_state = setup_bevy_app(&mut app, registry, &ExecutorOptions::default()); + + let join_handle = thread::spawn(move || { + app.run(); + }); + + WsTestFixture { + executor_state, + cleanup_test: move || { + send_stop.send(()).unwrap(); + join_handle.join().unwrap(); + }, + } + } + + #[ignore = "blocked on bevy_impulse https://github.com/open-rmf/bevy_impulse/issues/100"] + #[tokio::test] + #[test_log::test] + async fn test_ws_debug() { + let WsTestFixture { + executor_state, + cleanup_test, + } = setup_ws_test(); + + let mut diagram = new_add7_diagram(); + diagram.default_trace = bevy_impulse::TraceToggle::On; + + let request_body = PostRunRequest { + diagram, + request: serde_json::Value::from(5), + }; + + // Need to use "futures" channels rather than "tokio" channels as they implement `Sink` and + // `Stream` + let (socket_write, mut test_rx) = futures_channel::mpsc::channel(1024); + let (mut test_tx, socket_read) = futures_channel::mpsc::channel(1024); + + tokio::spawn(ws_debug(socket_write, socket_read, State(executor_state))); + + test_tx + .send(Ok(ws::Message::Text( + serde_json::to_string(&request_body).unwrap().into(), + ))) + .await + .unwrap(); + + // there should be 2 feedback messages + for _ in 0..2 { + let feedback_msg = test_rx.next().await.unwrap(); + let feedback: DebugSessionFeedback = + serde_json::from_slice(feedback_msg.into_text().unwrap().as_bytes()).unwrap(); + assert!(matches!( + feedback, + DebugSessionFeedback::OperationStarted(_) + )); + } + + let resp_msg = test_rx.next().await.unwrap(); + let resp_text = resp_msg.into_text().unwrap(); + let resp: DebugSessionEnd = serde_json::from_slice(resp_text.as_bytes()).unwrap(); + let resp = match resp { + DebugSessionEnd::Ok(resp) => resp, + _ => { + panic!("expected response to be Ok"); + } + }; + assert_eq!(resp, serde_json::Value::from(12)); + + cleanup_test(); + } +} diff --git a/diagram-editor/server/api/mod.rs b/diagram-editor/server/api/mod.rs new file mode 100644 index 00000000..7b1864fd --- /dev/null +++ b/diagram-editor/server/api/mod.rs @@ -0,0 +1,114 @@ +use axum::{ + http::{header, StatusCode}, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use bevy_impulse::DiagramElementRegistry; +use mime_guess::mime; + +mod error_responses; +pub mod executor; +mod websocket; + +#[derive(Default)] +#[non_exhaustive] +pub struct ApiOptions { + pub executor: executor::ExecutorOptions, +} + +#[derive(Clone)] +pub struct RegistryResponse(String); + +impl IntoResponse for RegistryResponse { + fn into_response(self) -> Response { + Response::builder() + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) + .body(self.0.into()) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR.into_response()) + } +} + +#[cfg(feature = "json_schema")] +impl schemars::JsonSchema for RegistryResponse { + fn schema_name() -> std::borrow::Cow<'static, str> { + DiagramElementRegistry::schema_name() + } + + fn schema_id() -> std::borrow::Cow<'static, str> { + DiagramElementRegistry::schema_id() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + DiagramElementRegistry::json_schema(generator) + } + + fn inline_schema() -> bool { + DiagramElementRegistry::inline_schema() + } +} + +impl RegistryResponse { + fn new(value: &DiagramElementRegistry) -> serde_json::Result { + let serialized = serde_json::to_string(value)?; + Ok(Self(serialized)) + } +} + +pub fn api_router( + app: &mut bevy_app::App, + registry: DiagramElementRegistry, + options: ApiOptions, +) -> Router { + let registry_resp = RegistryResponse::new(®istry).expect("failed to serialize registry"); + Router::new().route("/registry", get(registry_resp)).nest( + "/executor", + executor::new_router(app, registry, options.executor), + ) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::{self, Body}, + http::{header, Request, StatusCode}, + }; + use bevy_impulse::NodeBuilderOptions; + use mime_guess::mime; + use tower::Service; + + use super::*; + + #[tokio::test] + async fn test_serves_registry() { + let mut app = bevy_app::App::new(); + let mut registry = DiagramElementRegistry::new(); + registry.register_node_builder(NodeBuilderOptions::new("x2"), |builder, _config: ()| { + builder.create_map_block(|req: f64| req * 2.0) + }); + let mut router = api_router(&mut app, registry, ApiOptions::default()); + + let path = "/registry"; + let response = router + .call(Request::builder().uri(path).body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap(), + mime::APPLICATION_JSON + ); + + let resp_bytes = body::to_bytes(response.into_body(), 1024 * 1024) + .await + .unwrap(); + let resp_str = str::from_utf8(&resp_bytes).unwrap(); + let resp_registry: serde_json::Value = serde_json::from_str(resp_str).unwrap(); + assert!(resp_registry.get("nodes").unwrap().get("x2").is_some()); + } +} diff --git a/diagram-editor/server/api/websocket.rs b/diagram-editor/server/api/websocket.rs new file mode 100644 index 00000000..f807d622 --- /dev/null +++ b/diagram-editor/server/api/websocket.rs @@ -0,0 +1,76 @@ +use axum::extract::ws::{Message, Utf8Bytes}; +use futures_util::{Sink, SinkExt, Stream, StreamExt}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::Display; +use tracing::debug; + +pub(super) trait WebsocketStreamExt { + async fn next_text(&mut self) -> Option; + + async fn next_json(&mut self) -> Option { + let text = self.next_text().await?; + match serde_json::from_slice(text.as_bytes()) { + Ok(value) => Some(value), + Err(err) => { + debug!("{}", err); + None + } + } + } +} + +impl WebsocketStreamExt for S +where + S: Stream> + Unpin, + T: DeserializeOwned, +{ + async fn next_text(&mut self) -> Option { + let msg = if let Some(msg) = self.next().await { + match msg { + Ok(msg) => msg, + Err(err) => { + debug!("{}", err); + return None; + } + } + } else { + return None; + }; + let msg_text = match msg.into_text() { + Ok(text) => text, + Err(err) => { + debug!("{}", err); + return None; + } + }; + Some(msg_text) + } +} + +pub(super) trait WebsocketSinkExt { + async fn send_json(&mut self, value: &T) -> Option<()>; +} + +impl WebsocketSinkExt for S +where + S: Sink + Unpin, + S::Error: Display, + T: Serialize, +{ + async fn send_json(&mut self, value: &T) -> Option<()> { + let json_str = match serde_json::to_string(value).into() { + Ok(json_str) => json_str, + Err(err) => { + debug!("{}", err); + return None; + } + }; + match self.send(Message::Text(json_str.into())).await { + Ok(_) => Some(()), + Err(err) => { + debug!("{}", err); + None + } + } + } +} diff --git a/diagram-editor/server/bin/print_schema.rs b/diagram-editor/server/bin/print_schema.rs new file mode 100644 index 00000000..1d839835 --- /dev/null +++ b/diagram-editor/server/bin/print_schema.rs @@ -0,0 +1,27 @@ +use bevy_impulse_diagram_editor::api::{ + executor::{DebugSessionMessage, PostRunRequest}, + RegistryResponse, +}; +use indexmap::IndexMap; +use schemars::SchemaGenerator; + +fn main() { + let mut schema_generator = SchemaGenerator::default(); + schema_generator.subschema_for::(); + schema_generator.subschema_for::(); + schema_generator.subschema_for::(); + + // using `IndexMap` to preserve ordering + let schema: IndexMap<&'static str, serde_json::Value> = IndexMap::from_iter([ + ( + "$schema", + "https://json-schema.org/draft/2020-12/schema".into(), + ), + ( + "$defs", + serde_json::to_value(schema_generator.definitions()).unwrap(), + ), + ]); + + println!("{}", serde_json::to_string_pretty(&schema).unwrap(),); +} diff --git a/diagram-editor/server/frontend.rs b/diagram-editor/server/frontend.rs new file mode 100644 index 00000000..c2df8394 --- /dev/null +++ b/diagram-editor/server/frontend.rs @@ -0,0 +1,141 @@ +use axum::{ + body::Body, + extract::Path, + http::{header, StatusCode}, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use flate2::read::GzDecoder; +use std::{collections::HashMap, io::Read}; +use tar::Archive; + +// This will include the bytes of the dist.tar.gz file from the OUT_DIR +// The path is constructed at compile time. +const DIST_TAR_GZ: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/dist.tar.gz")); + +fn load_dist() -> HashMap> { + let mut archive = Archive::new(GzDecoder::new(DIST_TAR_GZ)); + let mut files = HashMap::new(); + + for entry_result in archive + .entries() + .expect("Failed to read entries from tar.gz") + { + let mut entry = entry_result.expect("Failed to get entry from tar.gz"); + let path = entry + .path() + .expect("Failed to get path from entry") + .into_owned(); + // Paths from tar, given build.rs `append_dir_all(".", "dist")`, will be like "index.html", "js/app.js" + let mut data = Vec::new(); + entry + .read_to_end(&mut data) + .expect("Failed to read entry data"); + files.insert(path, data); + } + if !files.contains_key(&std::path::PathBuf::from("index.html")) { + eprintln!( + "Warning: 'index.html' not found in embedded DIST assets. SPA fallback might not work." + ); + } + files +} + +async fn handle_text_html(html: Vec) -> impl IntoResponse { + let mime_type = "text/html"; + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, mime_type) + .body(Body::from(html)) + .unwrap_or_else(|_| internal_server_error_response()) +} + +async fn handle_asset( + dist: HashMap>, + Path(path_str): Path, +) -> impl IntoResponse { + let requested_file_path = std::path::PathBuf::from(path_str); + + // Attempt to serve the specific file + if let Some(file_bytes) = dist.get(&requested_file_path) { + let mime_type = mime_guess::from_path(&requested_file_path) + .first_or_octet_stream() + .to_string(); + + return Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, mime_type) + .body(Body::from(file_bytes.clone())) + .unwrap_or_else(|_| internal_server_error_response()); + } + + return Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("Not found")) + .unwrap_or_else(|_| internal_server_error_response()); +} + +fn internal_server_error_response() -> Response { + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("Internal Server Error")) + .unwrap() // Should not fail +} + +pub fn with_frontend_routes(router: Router) -> Router { + let dist = load_dist(); + let index_html = dist + .get(std::path::Path::new("index.html")) + .expect("index.html not found in dist") + .clone(); + + router + .route("/", get(move || handle_text_html(index_html))) + .route("/{*path}", get(move |path| handle_asset(dist, path))) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{ + body::Body, + http::{header, Request, StatusCode}, + response::Response, + }; + use tower::Service; + + fn assert_index_response_headers(response: &Response) { + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get(header::CONTENT_TYPE) + .expect("Content-Type header missing") + .to_str() + .unwrap(), + "text/html" + ); + } + + #[tokio::test] + async fn test_serves_index_html_with_root_url() { + let mut router = with_frontend_routes(Router::new()); + let response = router + .call(Request::builder().uri("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_index_response_headers(&response); + } + + #[tokio::test] + async fn test_serves_index_html_with_direct_path() { + let path = "/index.html"; + let mut router = with_frontend_routes(Router::new()); + let response = router + .call(Request::builder().uri(path).body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_index_response_headers(&response); + } +} diff --git a/diagram-editor/server/lib.rs b/diagram-editor/server/lib.rs new file mode 100644 index 00000000..27696c6f --- /dev/null +++ b/diagram-editor/server/lib.rs @@ -0,0 +1,28 @@ +use axum::Router; +use bevy_impulse::DiagramElementRegistry; + +use crate::api::{api_router, ApiOptions}; + +pub mod api; +#[cfg(feature = "frontend")] +mod frontend; + +#[derive(Default)] +#[non_exhaustive] +pub struct ServerOptions { + pub api: ApiOptions, +} + +/// Create a new [`axum::Router`] with routes for the diagram editor. +pub fn new_router( + app: &mut bevy_app::App, + registry: DiagramElementRegistry, + options: ServerOptions, +) -> Router { + let router = Router::new(); + + #[cfg(feature = "frontend")] + let router = frontend::with_frontend_routes(router); + + router.nest("/api", api_router(app, registry, options.api)) +} diff --git a/diagram-editor/tsconfig.json b/diagram-editor/tsconfig.json new file mode 100644 index 00000000..5b4330aa --- /dev/null +++ b/diagram-editor/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2020"], + "jsx": "react-jsx", + "target": "ES2020", + "noEmit": true, + "skipLibCheck": true, + "useDefineForClassFields": true, + + /* modules */ + "module": "ESNext", + "isolatedModules": true, + "resolveJsonModule": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "esModuleInterop": true, + + /* type checking */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["frontend"] +} diff --git a/examples/diagram/calculator/Cargo.toml b/examples/diagram/calculator/Cargo.toml index 8e89632b..8327e88b 100644 --- a/examples/diagram/calculator/Cargo.toml +++ b/examples/diagram/calculator/Cargo.toml @@ -4,13 +4,25 @@ version = "0.1.0" edition = "2021" [dependencies] +axum = "0.8.4" bevy_app = "0.12" bevy_core = "0.12" bevy_impulse = { version = "0.0.2", path = "../../..", features = ["diagram"] } +bevy_impulse_diagram_editor = { version = "0.0.1", path = "../../../diagram-editor", default-features = false, optional = true} bevy_time = "0.12" clap = { version = "4.5.23", features = ["derive"] } serde_json = "1.0.128" +tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.19" [dev-dependencies] assert_cmd = "2.0.16" + +[features] +default = ["api", "frontend"] +api = ["dep:bevy_impulse_diagram_editor"] +frontend = [ + "api", + "bevy_impulse_diagram_editor?/frontend", +] + diff --git a/examples/diagram/calculator/src/main.rs b/examples/diagram/calculator/src/main.rs index 4b897adb..a02c73fb 100644 --- a/examples/diagram/calculator/src/main.rs +++ b/examples/diagram/calculator/src/main.rs @@ -15,17 +15,38 @@ * */ -use std::{error::Error, fs::File, str::FromStr}; - +use bevy_app; use bevy_impulse::{ - Diagram, DiagramElementRegistry, DiagramError, ImpulsePlugin, NodeBuilderOptions, Promise, - RequestExt, RunCommandsOnWorldExt, + Diagram, DiagramElementRegistry, DiagramError, ImpulseAppPlugin, JsonMessage, + NodeBuilderOptions, Promise, RequestExt, RunCommandsOnWorldExt, }; +use bevy_impulse_diagram_editor::{new_router, ServerOptions}; use clap::Parser; +use serde_json::{Number, Value}; +use std::{error::Error, fs::File, str::FromStr, thread}; #[derive(Parser, Debug)] -/// Example calculator app using diagrams. -struct Args { +#[clap( + name = "calculator", + version = "0.1.0", + about = "Example calculator app using diagrams." +)] +struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Parser, Debug)] +enum Commands { + /// Runs a diagram with the given request. + Run(RunArgs), + + /// Starts a server to edit and run diagrams. + Serve(ServeArgs), +} + +#[derive(Parser, Debug)] +struct RunArgs { #[arg(help = "path to the diagram to run")] diagram: String, @@ -33,46 +54,244 @@ struct Args { request: String, } -fn main() -> Result<(), Box> { - let args = Args::parse(); +#[derive(Parser, Debug)] +struct ServeArgs { + #[arg(short, long, default_value_t = 3000)] + port: u16, +} - tracing_subscriber::fmt::init(); +fn run(args: RunArgs, registry: DiagramElementRegistry) -> Result<(), Box> { + let mut app = bevy_app::App::new(); + app.add_plugins(ImpulseAppPlugin::default()); + let file = File::open(args.diagram).unwrap(); + let diagram = Diagram::from_reader(file)?; + + let request = serde_json::Value::from_str(&args.request)?; + let mut promise = + app.world + .command(|cmds| -> Result, DiagramError> { + let workflow = diagram.spawn_io_workflow(cmds, ®istry)?; + Ok(cmds.request(request, workflow).take_response()) + })?; + + while promise.peek().is_pending() { + app.update(); + } + println!("{}", promise.take().available().unwrap()); + Ok(()) +} + +async fn serve(args: ServeArgs, registry: DiagramElementRegistry) -> Result<(), Box> { + println!("Serving diagram editor at http://localhost:{}", args.port); + + let mut app = bevy_app::App::new(); + app.add_plugins(ImpulseAppPlugin::default()); + let router = new_router(&mut app, registry, ServerOptions::default()); + thread::spawn(move || app.run()); + let listener = tokio::net::TcpListener::bind(("localhost", args.port)) + .await + .unwrap(); + axum::serve(listener, router).await?; + Ok(()) +} + +fn create_registry() -> DiagramElementRegistry { let mut registry = DiagramElementRegistry::new(); registry.register_node_builder( NodeBuilderOptions::new("add").with_default_display_text("Add"), - |builder, config: f64| builder.create_map_block(move |req: f64| req + config), + |builder, config: Option| { + builder.create_map_block(move |req: JsonMessage| { + let input = match req { + JsonMessage::Array(array) => { + let mut sum: f64 = 0.0; + for item in array + .iter() + .filter_map(Value::as_number) + .filter_map(Number::as_f64) + { + sum += item; + } + sum + } + JsonMessage::Number(number) => number.as_f64().unwrap_or(0.0), + _ => 0.0, + }; + + input + config.unwrap_or(0.0) + }) + }, ); + registry.register_node_builder( NodeBuilderOptions::new("sub").with_default_display_text("Subtract"), - |builder, config: f64| builder.create_map_block(move |req: f64| req - config), + |builder, config: Option| { + builder.create_map_block(move |req: JsonMessage| { + let input = match req { + JsonMessage::Array(array) => { + let mut iter = array + .iter() + .filter_map(Value::as_number) + .filter_map(Number::as_f64); + let mut input = iter.next().unwrap_or(0.0); + for item in iter { + input -= item; + } + input + } + JsonMessage::Number(number) => number.as_f64().unwrap_or(0.0), + _ => 0.0, + }; + + input - config.unwrap_or(0.0) + }) + }, ); + registry.register_node_builder( NodeBuilderOptions::new("mul").with_default_display_text("Multiply"), - |builder, config: f64| builder.create_map_block(move |req: f64| req * config), + |builder, config: Option| { + builder.create_map_block(move |req: JsonMessage| { + let input = match req { + JsonMessage::Array(array) => { + let mut iter = array + .iter() + .filter_map(Value::as_number) + .filter_map(Number::as_f64); + let mut input = iter.next().unwrap_or(0.0); + for item in iter { + input *= item; + } + input + } + JsonMessage::Number(number) => number.as_f64().unwrap_or(0.0), + _ => 0.0, + }; + + input * config.unwrap_or(1.0) + }) + }, ); + registry.register_node_builder( NodeBuilderOptions::new("div").with_default_display_text("Divide"), - |builder, config: f64| builder.create_map_block(move |req: f64| req / config), + |builder, config: Option| { + builder.create_map_block(move |req: JsonMessage| { + let input = match req { + JsonMessage::Array(array) => { + let mut iter = array + .iter() + .filter_map(Value::as_number) + .filter_map(Number::as_f64); + let mut input = iter.next().unwrap_or(0.0); + for item in iter { + input /= item; + } + input + } + JsonMessage::Number(number) => number.as_f64().unwrap_or(0.0), + _ => 0.0, + }; + + input / config.unwrap_or(1.0) + }) + }, ); + registry +} - let mut app = bevy_app::App::new(); - app.add_plugins(ImpulsePlugin::default()); - let file = File::open(args.diagram).unwrap(); - let diagram = Diagram::from_reader(file)?; +#[tokio::main] +async fn main() -> Result<(), Box> { + let cli = Cli::parse(); - let request = serde_json::Value::from_str(&args.request)?; - let mut promise = - app.world - .command(|cmds| -> Result, DiagramError> { + tracing_subscriber::fmt::init(); + + let registry = create_registry(); + + match cli.command { + Commands::Run(args) => run(args, registry), + Commands::Serve(args) => serve(args, registry).await, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_split() { + let diagram = Diagram::from_json(json!( + { + "$schema": "https://raw.githubusercontent.com/open-rmf/bevy_impulse/refs/heads/main/diagram.schema.json", + "version": "0.1.0", + "templates": {}, + "start": "62746cc5-19e4-456f-a94f-d49619ccd2c0", + "ops": { + "1283dab4-aec4-41d9-8dd3-ee04b2dc4c2b": { + "type": "node", + "builder": "mul", + "next": "49939e88-eefb-4dcc-8185-c880abe43cc6", + "config": 10 + }, + "2115dca9-fa22-406a-b4d1-6755732f9e4d": { + "type": "node", + "builder": "mul", + "next": "aa4fdf7b-a554-4683-82de-0bfbf3752d1d", + "config": 100 + }, + "62746cc5-19e4-456f-a94f-d49619ccd2c0": { + "type": "split", + "sequential": [ + "1283dab4-aec4-41d9-8dd3-ee04b2dc4c2b", + "2115dca9-fa22-406a-b4d1-6755732f9e4d" + ] + }, + "49939e88-eefb-4dcc-8185-c880abe43cc6": { + "type": "buffer" + }, + "aa4fdf7b-a554-4683-82de-0bfbf3752d1d": { + "type": "buffer" + }, + "f26502a3-84ec-4c4d-9367-15c099248640": { + "type": "node", + "builder": "add", + "next": { + "builtin": "terminate" + } + }, + "c3f84b5f-5f09-4dc7-b937-bd1cdf504bf9": { + "type": "join", + "buffers": [ + "49939e88-eefb-4dcc-8185-c880abe43cc6", + "aa4fdf7b-a554-4683-82de-0bfbf3752d1d" + ], + "next": "f26502a3-84ec-4c4d-9367-15c099248640" + } + } + } + )) + .unwrap(); + + let request = json!([1, 2]); + + let mut app = bevy_app::App::new(); + app.add_plugins(ImpulseAppPlugin::default()); + let registry = create_registry(); + + let mut promise = app + .world + .command(|cmds| -> Result, DiagramError> { let workflow = diagram.spawn_io_workflow(cmds, ®istry)?; Ok(cmds.request(request, workflow).take_response()) - })?; + }) + .unwrap(); - while promise.peek().is_pending() { - app.update(); - } + while promise.peek().is_pending() { + app.update(); + } - println!("{}", promise.take().available().unwrap()); - Ok(()) + let result = promise.take().available().unwrap().as_f64().unwrap(); + assert_eq!(result, 210.0); + } } diff --git a/examples/diagram/calculator/tests/e2e.rs b/examples/diagram/calculator/tests/e2e.rs index f0b0e223..2bbca126 100644 --- a/examples/diagram/calculator/tests/e2e.rs +++ b/examples/diagram/calculator/tests/e2e.rs @@ -4,7 +4,7 @@ use assert_cmd::Command; fn multiply3() { Command::cargo_bin("calculator") .unwrap() - .args(["multiply3.json", "4"]) + .args(["run", "multiply3.json", "4"]) .assert() .stdout("12.0\n"); } diff --git a/package.json b/package.json new file mode 100644 index 00000000..5b611f95 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "@biomejs/biome": "^2.0.5", + "typescript": "^5.8.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..0ea8af75 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7142 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@biomejs/biome': + specifier: ^2.0.5 + version: 2.0.5 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + + diagram-editor: + dependencies: + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@19.1.5)(react@19.1.0) + '@emotion/styled': + specifier: ^11.14.0 + version: 11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0) + '@fontsource/roboto': + specifier: ^5.2.5 + version: 5.2.5 + '@material-symbols/font-400': + specifier: ^0.31.9 + version: 0.31.9 + '@mui/material': + specifier: ^7.1.0 + version: 7.1.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/dagre': + specifier: ^0.7.53 + version: 0.7.53 + '@xyflow/react': + specifier: ^12.6.1 + version: 12.6.4(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + ajv: + specifier: ^8.17.1 + version: 8.17.1 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.17.1) + cel-js: + specifier: ^0.7.1 + version: 0.7.1 + dagre: + specifier: ^0.8.5 + version: 0.8.5 + fast-deep-equal: + specifier: ^3.1.3 + version: 3.1.3 + fflate: + specifier: ^0.8.2 + version: 0.8.2 + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + devDependencies: + '@rsbuild/core': + specifier: ^1.3.15 + version: 1.3.21 + '@rsbuild/plugin-react': + specifier: ^1.3.0 + version: 1.3.1(@rsbuild/core@1.3.21) + '@storybook/preview-api': + specifier: ^8.6.14 + version: 8.6.14(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3)) + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/react': + specifier: ^19.1.2 + version: 19.1.5 + '@types/react-dom': + specifier: ^19.1.3 + version: 19.1.5(@types/react@19.1.5) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0) + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0 + json-schema-to-typescript: + specifier: 15.0.4 + version: 15.0.4 + storybook: + specifier: ^9.0.9 + version: 9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3) + storybook-react-rsbuild: + specifier: ^2.0.1 + version: 2.0.1(@rsbuild/core@1.3.21)(@rspack/core@1.3.11(@swc/helpers@0.5.17))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.43.0)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)) + ts-jest: + specifier: ^29.3.4 + version: 29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(esbuild@0.25.5)(jest@29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0))(typescript@5.8.3) + +packages: + + '@adobe/css-tools@4.4.3': + resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@apidevtools/json-schema-ref-parser@11.9.3': + resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} + engines: {node: '>= 16'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.27.2': + resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.27.1': + resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.1': + resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.1': + resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.1': + resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.2': + resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.1': + resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.1': + resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.1': + resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@biomejs/biome@2.0.5': + resolution: {integrity: sha512-MztFGhE6cVjf3QmomWu83GpTFyWY8KIcskgRf2AqVEMSH4qI4rNdBLdpAQ11TNK9pUfLGz3IIOC1ZYwgBePtig==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.0.5': + resolution: {integrity: sha512-VIIWQv9Rcj9XresjCf3isBFfWjFStsdGZvm8SmwJzKs/22YQj167ge7DkxuaaZbNf2kmYif0AcjAKvtNedEoEw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.0.5': + resolution: {integrity: sha512-DRpGxBgf5Z7HUFcNUB6n66UiD4VlBlMpngNf32wPraxX8vYU6N9cb3xQWOXIQVBBQ64QfsSLJnjNu79i/LNmSg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.0.5': + resolution: {integrity: sha512-OpflTCOw/ElEs7QZqN/HFaSViPHjAsAPxFJ22LhWUWvuJgcy/Z8+hRV0/3mk/ZRWy5A6fCDKHZqAxU+xB6W4mA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.0.5': + resolution: {integrity: sha512-FQTfDNMXOknf8+g9Eede2daaduRjTC2SNbfWPNFMadN9K3UKjeZ62jwiYxztPaz9zQQsZU8VbddQIaeQY5CmIA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.0.5': + resolution: {integrity: sha512-9lmjCnajAzpZXbav2P6D87ugkhnaDpJtDvOH5uQbY2RXeW6Rq18uOUltxgacGBP+d8GusTr+s3IFOu7SN0Ok8g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.0.5': + resolution: {integrity: sha512-znpfydUDPuDkyBTulnODrQVK2FaG/4hIOPcQSsF2GeauQOYrBAOplj0etGB0NUrr0dFsvaQ15nzDXYb60ACoiw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.0.5': + resolution: {integrity: sha512-CP2wKQB+gh8HdJTFKYRFETqReAjxlcN9AlYDEoye8v2eQp+L9v+PUeDql/wsbaUhSsLR0sjj3PtbBtt+02AN3A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.0.5': + resolution: {integrity: sha512-Sw3rz2m6bBADeQpr3+MD7Ch4E1l15DTt/+dfqKnwkm3cn4BrYwnArmvKeZdVsFRDjMyjlKIP88bw1r7o+9aqzw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.3.1': + resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.0': + resolution: {integrity: sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fontsource/roboto@5.2.5': + resolution: {integrity: sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.2.0': + resolution: {integrity: sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.6.0': + resolution: {integrity: sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@material-symbols/font-400@0.31.9': + resolution: {integrity: sha512-PWfSdCjHoxTUQZEq3LaNPMJiyePoewOhm9kXqNHg6PDLPO8I6nmKYlaJB5b9KpEFGAPPcmXDosheMQiTYrQK/Q==} + + '@mdx-js/react@3.1.0': + resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@module-federation/error-codes@0.13.1': + resolution: {integrity: sha512-azgGDBnFRfqlivHOl96ZjlFUFlukESz2Rnnz/pINiSqoBBNjUE0fcAZP4X6jgrVITuEg90YkruZa7pW9I3m7Uw==} + + '@module-federation/runtime-core@0.13.1': + resolution: {integrity: sha512-TfyKfkSAentKeuvSsAItk8s5tqQSMfIRTPN2e1aoaq/kFhE+7blps719csyWSX5Lg5Es7WXKMsXHy40UgtBtuw==} + + '@module-federation/runtime-tools@0.13.1': + resolution: {integrity: sha512-GEF1pxqLc80osIMZmE8j9UKZSaTm2hX2lql8tgIH/O9yK4wnF06k6LL5Ah+wJt+oJv6Dj55ri/MoxMP4SXoPNA==} + + '@module-federation/runtime@0.13.1': + resolution: {integrity: sha512-ZHnYvBquDm49LiHfv6fgagMo/cVJneijNJzfPh6S0CJrPS2Tay1bnTXzy8VA5sdIrESagYPaskKMGIj7YfnPug==} + + '@module-federation/sdk@0.13.1': + resolution: {integrity: sha512-bmf2FGQ0ymZuxYnw9bIUfhV3y6zDhaqgydEjbl4msObKMLGXZqhse2pTIIxBFpIxR1oONKX/y2FAolDCTlWKiw==} + + '@module-federation/webpack-bundler-runtime@0.13.1': + resolution: {integrity: sha512-QSuSIGa09S8mthbB1L6xERqrz+AzPlHR6D7RwAzssAc+IHf40U6NiTLPzUqp9mmKDhC5Tm0EISU0ZHNeJpnpBQ==} + + '@mui/core-downloads-tracker@7.1.0': + resolution: {integrity: sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==} + + '@mui/material@7.1.0': + resolution: {integrity: sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material-pigment-css': ^7.1.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/material-pigment-css': + optional: true + '@types/react': + optional: true + + '@mui/private-theming@7.1.0': + resolution: {integrity: sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/styled-engine@7.1.0': + resolution: {integrity: sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/system@7.1.0': + resolution: {integrity: sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/types@7.4.2': + resolution: {integrity: sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@7.1.0': + resolution: {integrity: sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.43.0': + resolution: {integrity: sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.43.0': + resolution: {integrity: sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.43.0': + resolution: {integrity: sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.43.0': + resolution: {integrity: sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.43.0': + resolution: {integrity: sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.43.0': + resolution: {integrity: sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.43.0': + resolution: {integrity: sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.43.0': + resolution: {integrity: sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.43.0': + resolution: {integrity: sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.43.0': + resolution: {integrity: sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.43.0': + resolution: {integrity: sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.43.0': + resolution: {integrity: sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.43.0': + resolution: {integrity: sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.43.0': + resolution: {integrity: sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.43.0': + resolution: {integrity: sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.43.0': + resolution: {integrity: sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.43.0': + resolution: {integrity: sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.43.0': + resolution: {integrity: sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.43.0': + resolution: {integrity: sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.43.0': + resolution: {integrity: sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==} + cpu: [x64] + os: [win32] + + '@rsbuild/core@1.3.21': + resolution: {integrity: sha512-0Xy3CEFiLFXZpPmmVmX1XvfAENGrb5IyXYL7zkJ8vF7v3fmZgo3yy3ZeY8SesPTsiZIbCObJ6PemFbLee3S3oA==} + engines: {node: '>=16.10.0'} + hasBin: true + + '@rsbuild/plugin-react@1.3.1': + resolution: {integrity: sha512-1PfE0CZDwiSIUFaMFOEprwsHK6oo29zU6DdtFH2D49uLcpUdOUvU1u2p00RCVO1CIgnAjRajLS7dnPdQUwFOuQ==} + peerDependencies: + '@rsbuild/core': 1.x + + '@rsbuild/plugin-type-check@1.2.3': + resolution: {integrity: sha512-1yILSPgQFQCtY82f7CSbicIS/BqquoHgnDdAgPeYF3/k/RIwSAnclh0R2wXn+2EBormpFK82wz/TXuXl+k+evw==} + peerDependencies: + '@rsbuild/core': 1.x + peerDependenciesMeta: + '@rsbuild/core': + optional: true + + '@rspack/binding-darwin-arm64@1.3.11': + resolution: {integrity: sha512-sGoFDXYNinubhEiPSjtA/ua3qhMj6VVBPTSDvprZj+MT18YV7tQQtwBpm+8sbqJ1P5y+a3mzsP3IphRWyIQyXw==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@1.3.11': + resolution: {integrity: sha512-4zgOkCLxhp4Ki98GuDaZgz4exXcE4+sgvXY/xA/A5FGPVRbfQLQ5psSOk0F/gvMua1r15E66loQRJpuzUK6bTA==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@1.3.11': + resolution: {integrity: sha512-NIOaIfYUmJs1XL4lbGVtcMm1KlA/6ZR6oAbs2ekofKXtJYAFQgnLTf7ZFmIwVjS0mP78BmeSNcIM6pd2w5id4w==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-arm64-musl@1.3.11': + resolution: {integrity: sha512-CRRAQ379uzA2QfD9HHNtxuuqzGksUapMVcTLY5NIXWfvHLUJShdlSJQv3UQcqgAJNrMY7Ex1PnoQs1jZgUiqZA==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-x64-gnu@1.3.11': + resolution: {integrity: sha512-k3OyvLneX2ZeL8z/OzPojpImqy6PgqKJD+NtOvcr/TgbgADHZ3xQttf6B2X+qnZMAgOZ+RTeTkOFrvsg9AEKmA==} + cpu: [x64] + os: [linux] + + '@rspack/binding-linux-x64-musl@1.3.11': + resolution: {integrity: sha512-2agcELyyQ95jWGCW0YWD0TvAcN40yUjmxn9NXQBLHPX5Eb07NaHXairMsvV9vqQsPsq0nxxfd9Wsow18Y5r/Hw==} + cpu: [x64] + os: [linux] + + '@rspack/binding-win32-arm64-msvc@1.3.11': + resolution: {integrity: sha512-sjGoChazu0krigT/LVwGUsgCv3D3s/4cR/3P4VzuDNVlb4pbh1CDa642Fr0TceqAXCeKW5GiL/EQOfZ4semtcQ==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@1.3.11': + resolution: {integrity: sha512-tjywW84oQLSqRmvQZ+fXP7e3eNmjScYrlWEPAQFjf08N19iAJ9UOGuuFw8Fk5ZmrlNZ2Qo9ASSOI7Nnwx2aZYg==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@1.3.11': + resolution: {integrity: sha512-pPy3yU6SAMfEPY7ki1KAetiDFfRbkYMiX3F89P9kX01UAePkLRNsjacHF4w7N3EsBsWn1FlGaYZdlzmOI5pg2Q==} + cpu: [x64] + os: [win32] + + '@rspack/binding@1.3.11': + resolution: {integrity: sha512-BbMfZHqfH+CzFtZDg+v9nbKifJIJDUPD6KuoWlHq581koKvD3UMx6oVrj9w13JvO2xWNPeHclmqWAFgoD7faEQ==} + + '@rspack/core@1.3.11': + resolution: {integrity: sha512-aSYPtT1gum5MCfcFANdTroJ4JwzozuL3wX0twMGNAB7amq6+nZrbsUKWjcHgneCeZdahxzrKdyYef3FHaJ7lEA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@rspack/lite-tapable@1.0.1': + resolution: {integrity: sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==} + engines: {node: '>=16.0.0'} + + '@rspack/plugin-react-refresh@1.4.3': + resolution: {integrity: sha512-wZx4vWgy5oMEvgyNGd/oUKcdnKaccYWHCRkOqTdAPJC3WcytxhTX+Kady8ERurSBiLyQpoMiU3Iyd+F1Y2Arbw==} + peerDependencies: + react-refresh: '>=0.10.0 <1.0.0' + webpack-hot-middleware: 2.x + peerDependenciesMeta: + webpack-hot-middleware: + optional: true + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@storybook/addon-docs@9.0.9': + resolution: {integrity: sha512-jioo7RXTEzuCvZ1mD+7e2n6sGvlXH0mm8ENEsercM35vPKZzrmsmwQ+SPJk5GNjrcNy2qXR7bXJA14hwc+shuA==} + peerDependencies: + storybook: ^9.0.9 + + '@storybook/core-webpack@9.0.9': + resolution: {integrity: sha512-6aDu2rY7CFl6Erkyku7OrC8N4xkndncmwZjb7oGzrlwUnxehFNjQlZiJa96XwSef8oXdTCpU5HCToIOicJAvnw==} + peerDependencies: + storybook: ^9.0.9 + + '@storybook/csf-plugin@9.0.9': + resolution: {integrity: sha512-MvY2EOy/syxFykzgNJhpR+bkJFwyJ4lvUaZnU74/SIJSNc2gdbLDnx3J/AqXyNm0+VRHrBYvteiXLczE/1EtHg==} + peerDependencies: + storybook: ^9.0.9 + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@1.4.0': + resolution: {integrity: sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + + '@storybook/preview-api@8.6.14': + resolution: {integrity: sha512-2GhcCd4dNMrnD7eooEfvbfL4I83qAqEyO0CO7JQAmIO6Rxb9BsOLLI/GD5HkvQB73ArTJ+PT50rfaO820IExOQ==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/react-docgen-typescript-plugin@1.0.1': + resolution: {integrity: sha512-dqbHa+5gaxaklFCuV1WTvljVPTo3QIJgpW4Ln+QeME7osPZUnUhjN2/djvo+sxrWUrTTuqX5jkn291aDngu9Tw==} + peerDependencies: + typescript: '>= 3.x' + webpack: '>= 4' + + '@storybook/react-dom-shim@9.0.9': + resolution: {integrity: sha512-c2jvzpHW0EcYKhb7fvl3gh2waAnrNooZJasodxJXNhOIJWa6JkslxQXvhJsBkm24/nsvPvUthUP4hg7rA20a1A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.9 + + '@storybook/react@9.0.9': + resolution: {integrity: sha512-4yjbBClwCKxrzYm0nUUUEuONeVpnIN4xdzBrBF13ozn9KzLnlkNrj8bA8vPj5Ks8m7/AWkjHxV2e3VptRH15pA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.9 + typescript: '>= 4.9.x' + peerDependenciesMeta: + typescript: + optional: true + + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + + '@testing-library/dom@9.3.4': + resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} + engines: {node: '>=14'} + + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/dagre@0.7.53': + resolution: {integrity: sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==} + + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/html-minifier-terser@7.0.2': + resolution: {integrity: sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/jsdom@20.0.1': + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash@4.17.17': + resolution: {integrity: sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/node@18.19.111': + resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==} + + '@types/node@22.15.21': + resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==} + + '@types/node@24.0.4': + resolution: {integrity: sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.14': + resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + + '@types/react-dom@19.1.5': + resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@19.1.5': + resolution: {integrity: sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==} + + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@vitest/expect@3.0.9': + resolution: {integrity: sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==} + + '@vitest/pretty-format@3.0.9': + resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} + + '@vitest/spy@3.0.9': + resolution: {integrity: sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==} + + '@vitest/utils@3.0.9': + resolution: {integrity: sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + '@xyflow/react@12.6.4': + resolution: {integrity: sha512-/dOQ43Nu217cwHzy7f8kNUrFMeJJENzftVgT2VdFFHi6fHlG83pF+gLmvkRW9Be7alCsR6G+LFxxCdsQQbazHg==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.61': + resolution: {integrity: sha512-TsZG/Ez8dzxX6/Ol44LvFqVZsYvyz6dpDlAQZZk6hTL7JLGO5vN3dboRJqMwU8/Qtr5IEv5YBzojjAwIqW1HCA==} + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + better-opn@3.0.2: + resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} + engines: {node: '>=12.0.0'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-assert@1.2.1: + resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} + + browserslist@4.24.5: + resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.25.0: + resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001718: + resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} + + caniuse-lite@1.0.30001724: + resolution: {integrity: sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==} + + case-sensitive-paths-webpack-plugin@2.4.0: + resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} + engines: {node: '>=4'} + + cel-js@0.7.1: + resolution: {integrity: sha512-wTR2lnWtIoqdl7yxOc7rf2eNSe08NaoxoMyEFiU4J2AZvMj04gfU0hU50h5y4BphiWJZ5OMbAvN+9EKoGOlk4g==} + engines: {node: '>=18.0.0'} + + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + constants-browserify@1.0.0: + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js@3.42.0: + resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + dagre@0.8.5: + resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==} + + data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + + dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + + dedent@1.6.0: + resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.157: + resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==} + + electron-to-chromium@1.5.173: + resolution: {integrity: sha512-2bFhXP2zqSfQHugjqJIDFVwa+qIxyNApenmXTp9EjaKtdPrES5Qcn9/aSFy/NaP2E+fWG/zxKu/LBvY36p5VNQ==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + endent@2.1.0: + resolution: {integrity: sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==} + + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-parse@1.0.3: + resolution: {integrity: sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-cache-dir@5.0.0: + resolution: {integrity: sha512-OuWNfjfP05JcpAP3JPgAKUhWefjMRfI5iAoSsvE24ANYWJaepAtlSgWECSVEuRgSXpyNEc9DJwG/TZpgcOqyig==} + engines: {node: '>=16'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphlib@2.1.8: + resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier-terser@7.2.0: + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-jsdom@29.7.0: + resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-to-typescript@15.0.4: + resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==} + engines: {node: '>=16.0.0'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memfs@4.17.2: + resolution: {integrity: sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==} + engines: {node: '>= 4.0.0'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nwsapi@2.2.20: + resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + objectorarray@1.0.5: + resolution: {integrity: sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + ramda@0.30.1: + resolution: {integrity: sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@7.1.1: + resolution: {integrity: sha512-hlSJDQ2synMPKFZOsKo9Hi8WWZTC7POR8EmWvTSjow+VDgKzkmjQvFm2fk0tmRw+f0vTOIYKlarR0iL4996pdg==} + engines: {node: '>=16.14.0'} + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-is@19.1.0: + resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + reduce-configs@1.1.0: + resolution: {integrity: sha512-DQxy6liNadHfrLahZR7lMdc227NYVaQZhY5FMsxLEjX8X0SCuH+ESHSLCoz2yDZFq1/CLMDOAHdsEHwOEXKtvg==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.43.0: + resolution: {integrity: sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rsbuild-plugin-html-minifier-terser@1.1.1: + resolution: {integrity: sha512-rbDLv+XmGeSQo9JWKSwBst3Qwx1opLqtQCnQ3t9Z0F0ZTxKOC1S/ypPv5tSn/S3GMHct5Yb76mMgh6p80hjOAQ==} + peerDependencies: + '@rsbuild/core': 1.x || ^1.0.1-beta.0 + peerDependenciesMeta: + '@rsbuild/core': + optional: true + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + schema-utils@4.3.2: + resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + storybook-builder-rsbuild@2.0.1: + resolution: {integrity: sha512-8PHAiWPOI4f2vFmW8DyP19h/dw2MPbg3S5PpzZfZWZ/ZxDiia0WlgGAmqdRVmR2XS9xC1HsU/Dn3CCFXzMeC0g==} + peerDependencies: + '@rsbuild/core': ^1.0.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.0 + typescript: '*' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + typescript: + optional: true + + storybook-react-rsbuild@2.0.1: + resolution: {integrity: sha512-8k6sEIU1cCqOOpP3bg2Aq2T0vFOKmkUFsfE6oQrVMZkLABQvyAzhgpSRxfPpC7dgk3i2JRIP1oJNugNiWLMRKA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@rsbuild/core': ^1.0.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.0 + typescript: '>= 4.2.x' + peerDependenciesMeta: + typescript: + optional: true + + storybook@9.0.9: + resolution: {integrity: sha512-RYDKWD6X4ksYA+ASI1TRt2uB6681vhXGll5ofK9YUA5nrLd4hsp0yanNE2owMtaEhDATutpLKS+/+iFzPU8M2g==} + hasBin: true + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-indent@4.0.0: + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.3.14: + resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.42.0: + resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} + engines: {node: '>=10'} + hasBin: true + + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + thingies@1.21.0: + resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + + tree-dump@1.0.3: + resolution: {integrity: sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + ts-checker-rspack-plugin@1.1.4: + resolution: {integrity: sha512-lDpKuAubxUlsonUE1LpZS5fw7tfjutNb0lwjAo0k8OcxpWv/q18ytaD6eZXdjrFdTEFNIHtKp9dNkUKGky8SgA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@rspack/core': ^1.0.0 + typescript: '>=3.8.0' + peerDependenciesMeta: + '@rspack/core': + optional: true + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ts-jest@29.3.4: + resolution: {integrity: sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} + engines: {node: '>=14.0.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + engines: {node: '>=10.13.0'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + webpack@5.99.9: + resolution: {integrity: sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + ws@8.18.2: + resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@adobe/css-tools@4.4.3': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@apidevtools/json-schema-ref-parser@11.9.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.0 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.2': {} + + '@babel/core@7.27.1': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) + '@babel/helpers': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.1': + dependencies: + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.27.2 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.5 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.1': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 + + '@babel/parser@7.27.2': + dependencies: + '@babel/types': 7.27.1 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.27.1': {} + + '@babel/runtime@7.27.6': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + + '@babel/traverse@7.27.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 + debug: 4.4.1 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + + '@biomejs/biome@2.0.5': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.0.5 + '@biomejs/cli-darwin-x64': 2.0.5 + '@biomejs/cli-linux-arm64': 2.0.5 + '@biomejs/cli-linux-arm64-musl': 2.0.5 + '@biomejs/cli-linux-x64': 2.0.5 + '@biomejs/cli-linux-x64-musl': 2.0.5 + '@biomejs/cli-win32-arm64': 2.0.5 + '@biomejs/cli-win32-x64': 2.0.5 + + '@biomejs/cli-darwin-arm64@2.0.5': + optional: true + + '@biomejs/cli-darwin-x64@2.0.5': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.0.5': + optional: true + + '@biomejs/cli-linux-arm64@2.0.5': + optional: true + + '@biomejs/cli-linux-x64-musl@2.0.5': + optional: true + + '@biomejs/cli-linux-x64@2.0.5': + optional: true + + '@biomejs/cli-win32-arm64@2.0.5': + optional: true + + '@biomejs/cli-win32-x64@2.0.5': + optional: true + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.27.1 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.3.1': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.3.1 + '@emotion/react': 11.14.0(@types/react@19.1.5)(react@19.1.0) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) + '@emotion/utils': 1.4.2 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@fontsource/roboto@5.2.5': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.15.21 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 22.15.21 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.27.1 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.15.21 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jsdevtools/ono@7.1.3': {} + + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.2.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.6.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 1.21.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@1.6.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@material-symbols/font-400@0.31.9': {} + + '@mdx-js/react@3.1.0(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.1.5 + react: 19.1.0 + + '@module-federation/error-codes@0.13.1': {} + + '@module-federation/runtime-core@0.13.1': + dependencies: + '@module-federation/error-codes': 0.13.1 + '@module-federation/sdk': 0.13.1 + + '@module-federation/runtime-tools@0.13.1': + dependencies: + '@module-federation/runtime': 0.13.1 + '@module-federation/webpack-bundler-runtime': 0.13.1 + + '@module-federation/runtime@0.13.1': + dependencies: + '@module-federation/error-codes': 0.13.1 + '@module-federation/runtime-core': 0.13.1 + '@module-federation/sdk': 0.13.1 + + '@module-federation/sdk@0.13.1': {} + + '@module-federation/webpack-bundler-runtime@0.13.1': + dependencies: + '@module-federation/runtime': 0.13.1 + '@module-federation/sdk': 0.13.1 + + '@mui/core-downloads-tracker@7.1.0': {} + + '@mui/material@7.1.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@mui/core-downloads-tracker': 7.1.0 + '@mui/system': 7.1.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0) + '@mui/types': 7.4.2(@types/react@19.1.5) + '@mui/utils': 7.1.0(@types/react@19.1.5)(react@19.1.0) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@19.1.5) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-is: 19.1.0 + react-transition-group: 4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.1.5)(react@19.1.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0) + '@types/react': 19.1.5 + + '@mui/private-theming@7.1.0(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@mui/utils': 7.1.0(@types/react@19.1.5)(react@19.1.0) + prop-types: 15.8.1 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@mui/styled-engine@7.1.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.0 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.1.5)(react@19.1.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0) + + '@mui/system@7.1.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@mui/private-theming': 7.1.0(@types/react@19.1.5)(react@19.1.0) + '@mui/styled-engine': 7.1.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0))(react@19.1.0) + '@mui/types': 7.4.2(@types/react@19.1.5) + '@mui/utils': 7.1.0(@types/react@19.1.5)(react@19.1.0) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.0 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.1.5)(react@19.1.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.5)(react@19.1.0))(@types/react@19.1.5)(react@19.1.0) + '@types/react': 19.1.5 + + '@mui/types@7.4.2(@types/react@19.1.5)': + dependencies: + '@babel/runtime': 7.27.1 + optionalDependencies: + '@types/react': 19.1.5 + + '@mui/utils@7.1.0(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@mui/types': 7.4.2(@types/react@19.1.5) + '@types/prop-types': 15.7.14 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.1.0 + react-is: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@polka/url@1.0.0-next.29': {} + + '@popperjs/core@2.11.8': {} + + '@rollup/pluginutils@5.1.4(rollup@4.43.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.43.0 + + '@rollup/rollup-android-arm-eabi@4.43.0': + optional: true + + '@rollup/rollup-android-arm64@4.43.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.43.0': + optional: true + + '@rollup/rollup-darwin-x64@4.43.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.43.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.43.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.43.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.43.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.43.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.43.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.43.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.43.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.43.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.43.0': + optional: true + + '@rsbuild/core@1.3.21': + dependencies: + '@rspack/core': 1.3.11(@swc/helpers@0.5.17) + '@rspack/lite-tapable': 1.0.1 + '@swc/helpers': 0.5.17 + core-js: 3.42.0 + jiti: 2.4.2 + + '@rsbuild/plugin-react@1.3.1(@rsbuild/core@1.3.21)': + dependencies: + '@rsbuild/core': 1.3.21 + '@rspack/plugin-react-refresh': 1.4.3(react-refresh@0.17.0) + react-refresh: 0.17.0 + transitivePeerDependencies: + - webpack-hot-middleware + + '@rsbuild/plugin-type-check@1.2.3(@rsbuild/core@1.3.21)(@rspack/core@1.3.11(@swc/helpers@0.5.17))(typescript@5.8.3)': + dependencies: + deepmerge: 4.3.1 + json5: 2.2.3 + reduce-configs: 1.1.0 + ts-checker-rspack-plugin: 1.1.4(@rspack/core@1.3.11(@swc/helpers@0.5.17))(typescript@5.8.3) + optionalDependencies: + '@rsbuild/core': 1.3.21 + transitivePeerDependencies: + - '@rspack/core' + - typescript + + '@rspack/binding-darwin-arm64@1.3.11': + optional: true + + '@rspack/binding-darwin-x64@1.3.11': + optional: true + + '@rspack/binding-linux-arm64-gnu@1.3.11': + optional: true + + '@rspack/binding-linux-arm64-musl@1.3.11': + optional: true + + '@rspack/binding-linux-x64-gnu@1.3.11': + optional: true + + '@rspack/binding-linux-x64-musl@1.3.11': + optional: true + + '@rspack/binding-win32-arm64-msvc@1.3.11': + optional: true + + '@rspack/binding-win32-ia32-msvc@1.3.11': + optional: true + + '@rspack/binding-win32-x64-msvc@1.3.11': + optional: true + + '@rspack/binding@1.3.11': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.3.11 + '@rspack/binding-darwin-x64': 1.3.11 + '@rspack/binding-linux-arm64-gnu': 1.3.11 + '@rspack/binding-linux-arm64-musl': 1.3.11 + '@rspack/binding-linux-x64-gnu': 1.3.11 + '@rspack/binding-linux-x64-musl': 1.3.11 + '@rspack/binding-win32-arm64-msvc': 1.3.11 + '@rspack/binding-win32-ia32-msvc': 1.3.11 + '@rspack/binding-win32-x64-msvc': 1.3.11 + + '@rspack/core@1.3.11(@swc/helpers@0.5.17)': + dependencies: + '@module-federation/runtime-tools': 0.13.1 + '@rspack/binding': 1.3.11 + '@rspack/lite-tapable': 1.0.1 + caniuse-lite: 1.0.30001718 + optionalDependencies: + '@swc/helpers': 0.5.17 + + '@rspack/lite-tapable@1.0.1': {} + + '@rspack/plugin-react-refresh@1.4.3(react-refresh@0.17.0)': + dependencies: + error-stack-parser: 2.1.4 + html-entities: 2.6.0 + react-refresh: 0.17.0 + + '@sinclair/typebox@0.27.8': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@storybook/addon-docs@9.0.9(@types/react@19.1.5)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))': + dependencies: + '@mdx-js/react': 3.1.0(@types/react@19.1.5)(react@19.1.0) + '@storybook/csf-plugin': 9.0.9(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3)) + '@storybook/icons': 1.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@storybook/react-dom-shim': 9.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3)) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + storybook: 9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + + '@storybook/core-webpack@9.0.9(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))': + dependencies: + storybook: 9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3) + ts-dedent: 2.2.0 + + '@storybook/csf-plugin@9.0.9(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))': + dependencies: + storybook: 9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3) + unplugin: 1.16.1 + + '@storybook/global@5.0.0': {} + + '@storybook/icons@1.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@storybook/preview-api@8.6.14(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))': + dependencies: + storybook: 9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3) + + '@storybook/react-docgen-typescript-plugin@1.0.1(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5))': + dependencies: + debug: 4.4.1 + endent: 2.1.0 + find-cache-dir: 3.3.2 + flat-cache: 3.2.0 + micromatch: 4.0.8 + react-docgen-typescript: 2.4.0(typescript@5.8.3) + tslib: 2.8.1 + typescript: 5.8.3 + webpack: 5.99.9(esbuild@0.25.5) + transitivePeerDependencies: + - supports-color + + '@storybook/react-dom-shim@9.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))': + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + storybook: 9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3) + + '@storybook/react@9.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))(typescript@5.8.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 9.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3)) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + storybook: 9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3) + optionalDependencies: + typescript: 5.8.3 + + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + + '@testing-library/dom@9.3.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.27.6 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.6.3': + dependencies: + '@adobe/css-tools': 4.4.3 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/user-event@14.6.1(@testing-library/dom@9.3.4)': + dependencies: + '@testing-library/dom': 9.3.4 + + '@tootallnate/once@2.0.0': {} + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.27.1 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.27.1 + + '@types/d3-color@3.1.3': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/dagre@0.7.53': {} + + '@types/doctrine@0.0.9': {} + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.8 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.7': + optional: true + + '@types/estree@1.0.8': {} + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.15.21 + + '@types/html-minifier-terser@7.0.2': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/jsdom@20.0.1': + dependencies: + '@types/node': 22.15.21 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + + '@types/json-schema@7.0.15': {} + + '@types/lodash@4.17.17': {} + + '@types/mdx@2.0.13': {} + + '@types/node@18.19.111': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.15.21': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.0.4': + dependencies: + undici-types: 7.8.0 + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.14': {} + + '@types/react-dom@19.1.5(@types/react@19.1.5)': + dependencies: + '@types/react': 19.1.5 + + '@types/react-transition-group@4.4.12(@types/react@19.1.5)': + dependencies: + '@types/react': 19.1.5 + + '@types/react@19.1.5': + dependencies: + csstype: 3.1.3 + + '@types/resolve@1.20.6': {} + + '@types/stack-utils@2.0.3': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@vitest/expect@3.0.9': + dependencies: + '@vitest/spy': 3.0.9 + '@vitest/utils': 3.0.9 + chai: 5.2.0 + tinyrainbow: 2.0.0 + + '@vitest/pretty-format@3.0.9': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/spy@3.0.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@3.0.9': + dependencies: + '@vitest/pretty-format': 3.0.9 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + '@xyflow/react@12.6.4(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@xyflow/system': 0.0.61 + classcat: 5.0.5 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + zustand: 4.5.7(@types/react@19.1.5)(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.61': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + + abab@2.0.6: {} + + acorn-globals@7.0.1: + dependencies: + acorn: 8.14.1 + acorn-walk: 8.3.4 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + acorn@8.15.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + async@3.2.6: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + babel-jest@29.7.0(@babel/core@7.27.1): + dependencies: + '@babel/core': 7.27.1 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.27.1) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.7 + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.27.1 + cosmiconfig: 7.1.0 + resolve: 1.22.10 + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.27.1): + dependencies: + '@babel/core': 7.27.1 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.27.1) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.1) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.27.1) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.27.1) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.27.1) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.27.1) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.27.1) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.1) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.27.1) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.27.1) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.27.1) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.1) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.1) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.1) + + babel-preset-jest@29.6.3(@babel/core@7.27.1): + dependencies: + '@babel/core': 7.27.1 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.1) + + balanced-match@1.0.2: {} + + better-opn@3.0.2: + dependencies: + open: 8.4.2 + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-assert@1.2.1: {} + + browserslist@4.24.5: + dependencies: + caniuse-lite: 1.0.30001718 + electron-to-chromium: 1.5.157 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.5) + + browserslist@4.25.0: + dependencies: + caniuse-lite: 1.0.30001724 + electron-to-chromium: 1.5.173 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.0) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001718: {} + + caniuse-lite@1.0.30001724: {} + + case-sensitive-paths-webpack-plugin@2.4.0: {} + + cel-js@0.7.1: + dependencies: + chevrotain: 11.0.3 + ramda: 0.30.1 + + chai@5.2.0: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + check-error@2.1.1: {} + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chrome-trace-event@1.0.4: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.3: {} + + classcat@5.0.5: {} + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + commander@2.20.3: {} + + common-path-prefix@3.0.0: {} + + commondir@1.0.1: {} + + concat-map@0.0.1: {} + + constants-browserify@1.0.0: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + core-js@3.42.0: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + create-jest@29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css.escape@1.5.1: {} + + cssom@0.3.8: {} + + cssom@0.5.0: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + + csstype@3.1.3: {} + + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-selection@3.0.0: {} + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + dagre@0.8.5: + dependencies: + graphlib: 2.1.8 + lodash: 4.17.21 + + data-urls@3.0.2: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + decimal.js@10.5.0: {} + + dedent@0.7.0: {} + + dedent@1.6.0(babel-plugin-macros@3.1.0): + optionalDependencies: + babel-plugin-macros: 3.1.0 + + deep-eql@5.0.2: {} + + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + detect-newline@3.1.0: {} + + diff-sequences@29.6.3: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.27.1 + csstype: 3.1.3 + + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-to-chromium@1.5.157: {} + + electron-to-chromium@1.5.173: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + endent@2.1.0: + dependencies: + dedent: 0.7.0 + fast-json-parse: 1.0.3 + objectorarray: 1.0.5 + + enhanced-resolve@5.18.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + entities@4.5.0: {} + + entities@6.0.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild-register@3.6.0(esbuild@0.25.5): + dependencies: + debug: 4.4.1 + esbuild: 0.25.5 + transitivePeerDependencies: + - supports-color + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + escalade@3.2.0: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + esprima@4.0.1: {} + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + fast-deep-equal@3.1.3: {} + + fast-json-parse@1.0.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-uri@3.0.6: {} + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fflate@0.8.2: {} + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-cache-dir@5.0.0: + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 7.0.0 + + find-root@1.1.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@6.3.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@11.12.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphlib@2.1.8: + dependencies: + lodash: 4.17.21 + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + html-minifier-terser@7.2.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 10.0.1 + entities: 4.5.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.42.0 + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + hyperdyperid@1.2.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.27.1 + '@babel/parser': 7.27.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.27.1 + '@babel/parser': 7.27.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0(babel-plugin-macros@3.1.0): + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.6.0(babel-plugin-macros@3.1.0) + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0): + dependencies: + '@babel/core': 7.27.1 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.1) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.15.21 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-jsdom@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 22.15.21 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.15.21 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.10 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.1) + '@babel/types': 7.27.1 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.1) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.21 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 24.0.4 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.15.21 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jiti@2.4.2: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdom@20.0.3: + dependencies: + abab: 2.0.6 + acorn: 8.14.1 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.5.0 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.2 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.20 + parse5: 7.3.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.18.2 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-to-typescript@15.0.4: + dependencies: + '@apidevtools/json-schema-ref-parser': 11.9.3 + '@types/json-schema': 7.0.15 + '@types/lodash': 4.17.17 + is-glob: 4.0.3 + js-yaml: 4.1.0 + lodash: 4.17.21 + minimist: 1.2.8 + prettier: 3.5.3 + tinyglobby: 0.2.14 + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + leven@3.1.0: {} + + lines-and-columns@1.2.4: {} + + loader-runner@4.3.0: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.21: {} + + lodash.memoize@4.1.2: {} + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.1.3: {} + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + memfs@4.17.2: + dependencies: + '@jsonjoy.com/json-pack': 1.2.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.6.0(tslib@2.8.1) + tree-dump: 1.0.3(tslib@2.8.1) + tslib: 2.8.1 + + merge-stream@2.0.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-int64@0.4.0: {} + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nwsapi@2.2.20: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + objectorarray@1.0.5: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-try@2.2.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@7.3.0: + dependencies: + entities: 6.0.0 + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + pathval@2.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-dir@7.0.0: + dependencies: + find-up: 6.3.0 + + possible-typed-array-names@1.1.0: {} + + prettier@3.5.3: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + process@0.11.10: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@1.4.1: {} + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + querystringify@2.2.0: {} + + ramda@0.30.1: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + react-docgen-typescript@2.4.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + react-docgen@7.1.1: + dependencies: + '@babel/core': 7.27.1 + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.7 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.10 + strip-indent: 4.0.0 + transitivePeerDependencies: + - supports-color + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-is@19.1.0: {} + + react-refresh@0.17.0: {} + + react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/runtime': 7.27.1 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + react@19.1.0: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + reduce-configs@1.1.0: {} + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + relateurl@0.2.7: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.43.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.43.0 + '@rollup/rollup-android-arm64': 4.43.0 + '@rollup/rollup-darwin-arm64': 4.43.0 + '@rollup/rollup-darwin-x64': 4.43.0 + '@rollup/rollup-freebsd-arm64': 4.43.0 + '@rollup/rollup-freebsd-x64': 4.43.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.43.0 + '@rollup/rollup-linux-arm-musleabihf': 4.43.0 + '@rollup/rollup-linux-arm64-gnu': 4.43.0 + '@rollup/rollup-linux-arm64-musl': 4.43.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.43.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.43.0 + '@rollup/rollup-linux-riscv64-gnu': 4.43.0 + '@rollup/rollup-linux-riscv64-musl': 4.43.0 + '@rollup/rollup-linux-s390x-gnu': 4.43.0 + '@rollup/rollup-linux-x64-gnu': 4.43.0 + '@rollup/rollup-linux-x64-musl': 4.43.0 + '@rollup/rollup-win32-arm64-msvc': 4.43.0 + '@rollup/rollup-win32-ia32-msvc': 4.43.0 + '@rollup/rollup-win32-x64-msvc': 4.43.0 + fsevents: 2.3.3 + optional: true + + rsbuild-plugin-html-minifier-terser@1.1.1(@rsbuild/core@1.3.21): + dependencies: + '@types/html-minifier-terser': 7.0.2 + html-minifier-terser: 7.2.0 + optionalDependencies: + '@rsbuild/core': 1.3.21 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.26.0: {} + + schema-utils@4.3.2: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + + semver@6.3.1: {} + + semver@7.7.2: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stackframe@1.3.4: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + storybook-builder-rsbuild@2.0.1(@rsbuild/core@1.3.21)(@rspack/core@1.3.11(@swc/helpers@0.5.17))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))(typescript@5.8.3): + dependencies: + '@rsbuild/core': 1.3.21 + '@rsbuild/plugin-type-check': 1.2.3(@rsbuild/core@1.3.21)(@rspack/core@1.3.11(@swc/helpers@0.5.17))(typescript@5.8.3) + '@storybook/addon-docs': 9.0.9(@types/react@19.1.5)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3)) + '@storybook/core-webpack': 9.0.9(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3)) + browser-assert: 1.2.1 + case-sensitive-paths-webpack-plugin: 2.4.0 + cjs-module-lexer: 1.4.3 + constants-browserify: 1.0.0 + es-module-lexer: 1.7.0 + find-cache-dir: 5.0.0 + fs-extra: 11.3.0 + magic-string: 0.30.17 + path-browserify: 1.0.1 + process: 0.11.10 + rsbuild-plugin-html-minifier-terser: 1.1.1(@rsbuild/core@1.3.21) + sirv: 2.0.4 + storybook: 9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3) + ts-dedent: 2.2.0 + url: 0.11.4 + util: 0.12.5 + util-deprecate: 1.0.2 + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + typescript: 5.8.3 + transitivePeerDependencies: + - '@rspack/core' + - '@types/react' + + storybook-react-rsbuild@2.0.1(@rsbuild/core@1.3.21)(@rspack/core@1.3.11(@swc/helpers@0.5.17))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.43.0)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)): + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.43.0) + '@rsbuild/core': 1.3.21 + '@storybook/react': 9.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))(typescript@5.8.3) + '@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)) + '@types/node': 18.19.111 + find-up: 5.0.0 + magic-string: 0.30.17 + react: 19.1.0 + react-docgen: 7.1.1 + react-docgen-typescript: 2.4.0(typescript@5.8.3) + react-dom: 19.1.0(react@19.1.0) + resolve: 1.22.10 + storybook: 9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3) + storybook-builder-rsbuild: 2.0.1(@rsbuild/core@1.3.21)(@rspack/core@1.3.11(@swc/helpers@0.5.17))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3))(typescript@5.8.3) + tsconfig-paths: 4.2.0 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@rspack/core' + - '@types/react' + - rollup + - supports-color + - webpack + + storybook@9.0.9(@testing-library/dom@9.3.4)(prettier@3.5.3): + dependencies: + '@storybook/global': 5.0.0 + '@testing-library/jest-dom': 6.6.3 + '@testing-library/user-event': 14.6.1(@testing-library/dom@9.3.4) + '@vitest/expect': 3.0.9 + '@vitest/spy': 3.0.9 + better-opn: 3.0.2 + esbuild: 0.25.5 + esbuild-register: 3.6.0(esbuild@0.25.5) + recast: 0.23.11 + semver: 7.7.2 + ws: 8.18.2 + optionalDependencies: + prettier: 3.5.3 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - supports-color + - utf-8-validate + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-indent@4.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + stylis@4.2.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tapable@2.2.2: {} + + terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + terser: 5.43.1 + webpack: 5.99.9(esbuild@0.25.5) + optionalDependencies: + esbuild: 0.25.5 + + terser@5.42.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + terser@5.43.1: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + thingies@1.21.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + tinyrainbow@2.0.0: {} + + tinyspy@3.0.2: {} + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@3.0.0: + dependencies: + punycode: 2.3.1 + + tree-dump@1.0.3(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + + ts-checker-rspack-plugin@1.1.4(@rspack/core@1.3.11(@swc/helpers@0.5.17))(typescript@5.8.3): + dependencies: + '@babel/code-frame': 7.27.1 + '@rspack/lite-tapable': 1.0.1 + chokidar: 3.6.0 + is-glob: 4.0.3 + memfs: 4.17.2 + minimatch: 9.0.5 + picocolors: 1.1.1 + typescript: 5.8.3 + optionalDependencies: + '@rspack/core': 1.3.11(@swc/helpers@0.5.17) + + ts-dedent@2.2.0: {} + + ts-jest@29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(esbuild@0.25.5)(jest@29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0))(typescript@5.8.3): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.8.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.27.1 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.1) + esbuild: 0.25.5 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + typescript@5.8.3: {} + + undici-types@5.26.5: {} + + undici-types@6.21.0: {} + + undici-types@7.8.0: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + unplugin@1.16.1: + dependencies: + acorn: 8.14.1 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.1.3(browserslist@4.24.5): + dependencies: + browserslist: 4.24.5 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-browserslist-db@1.1.3(browserslist@4.25.0): + dependencies: + browserslist: 4.25.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.14.0 + + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + + uuid@11.1.0: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + w3c-xmlserializer@4.0.0: + dependencies: + xml-name-validator: 4.0.0 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watchpack@2.4.4: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + webidl-conversions@7.0.0: {} + + webpack-sources@3.3.3: {} + + webpack-virtual-modules@0.6.2: {} + + webpack@5.99.9(esbuild@0.25.5): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + browserslist: 4.25.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.2 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.2 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + whatwg-encoding@2.0.0: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@3.0.0: {} + + whatwg-url@11.0.0: + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@8.18.2: {} + + xml-name-validator@4.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@1.10.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} + + zustand@4.5.7(@types/react@19.1.5)(react@19.1.0): + dependencies: + use-sync-external-store: 1.5.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + react: 19.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..ba1340f2 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +packages: + - diagram-editor + +onlyBuiltDependencies: + - core-js + - esbuild diff --git a/src/diagram.rs b/src/diagram.rs index d5e9d58c..565eb7f0 100644 --- a/src/diagram.rs +++ b/src/diagram.rs @@ -866,8 +866,11 @@ pub enum DiagramErrorCode { #[error("one or more operation is missing inputs")] IncompleteDiagram, - #[error(transparent)] - JsonError(#[from] serde_json::Error), + #[error("the config of the operation has an error: {0}")] + ConfigError(serde_json::Error), + + #[error("failed to create trace info for the operation: {0}")] + TraceInfoError(serde_json::Error), #[error(transparent)] ConnectionError(#[from] SplitConnectionError), diff --git a/src/diagram/registration.rs b/src/diagram/registration.rs index b2bb6775..8ae425f1 100644 --- a/src/diagram/registration.rs +++ b/src/diagram/registration.rs @@ -181,7 +181,8 @@ impl<'a, DeserializeImpl, SerializeImpl, Cloneable> .schema_generator .subschema_for::(), create_node_impl: RefCell::new(Box::new(move |builder, config| { - let config = serde_json::from_value(config)?; + let config = + serde_json::from_value(config).map_err(DiagramErrorCode::ConfigError)?; Ok(f(builder, config).into()) })), }; diff --git a/src/diagram/workflow_builder.rs b/src/diagram/workflow_builder.rs index 12187cd8..7dfc4feb 100644 --- a/src/diagram/workflow_builder.rs +++ b/src/diagram/workflow_builder.rs @@ -1262,8 +1262,12 @@ impl TraceInfo { construction: impl Serialize, trace: Option, ) -> Result { + let construction = Some(Arc::new( + serde_json::to_value(construction).map_err(DiagramErrorCode::TraceInfoError)?, + )); + Ok(Self { - construction: Some(Arc::new(serde_json::to_value(construction)?)), + construction, trace, }) }