diff --git a/README.md b/README.md index e699e634..1eafaddf 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ - + @@ -101,79 +101,68 @@ Every diagramming tool makes a compromise. OpenFlowKit doesn't. | **Lucidchart / Miro** | Cloud lock-in โ€” expensive, account required, your data lives on their servers | | **PlantUML** | Server-dependent rendering โ€” no visual editor, no local-first model | -OpenFlowKit is the **only MIT-licensed tool** that combines a real workspace home, a professional visual canvas, bidirectional diagram-as-code, AI generation from 9 providers, deterministic and AI-assisted imports, asset libraries for technical diagrams, and cinematic animated export โ€” with zero server-side storage. +OpenFlowKit is the **only MIT-licensed tool** that combines a real workspace home, a professional visual canvas, bidirectional diagram-as-code, AI generation from 9 providers, **automatic icon assignment from 1,100+ tech icons**, and cinematic animated export โ€” with zero server-side storage. --- ## Feature highlights -| | OpenFlowKit | Excalidraw | Draw.io | Mermaid | Lucidchart | -| ------------------------------ | :---------: | :--------: | :-----: | :-----: | :--------: | -| Visual canvas editor | โœ… | โœ… | โœ… | โŒ | โœ… | -| Bidirectional diagram-as-code | โœ… | โŒ | โŒ | โœ… | โŒ | -| AI generation (9 providers) `Beta` | โœ… | โŒ | โŒ | โŒ | Limited | -| SQL โ†’ ERD (native parser) | โœ… | โŒ | โŒ | โŒ | โŒ | -| Terraform / K8s import `Beta` | โœ… | โŒ | โŒ | โŒ | โŒ | -| AWS / Azure / GCP / CNCF icons | โœ… | โŒ | โœ… | Partial | โœ… | +| | OpenFlowKit | Excalidraw | Draw.io | Mermaid | Lucidchart | +| ------------------------------------ | :---------: | :--------: | :-----: | :-----: | :--------: | +| Visual canvas editor | โœ… | โœ… | โœ… | โŒ | โœ… | +| Bidirectional diagram-as-code | โœ… | โŒ | โŒ | โœ… | โŒ | +| AI generation (9 providers) `Beta` | โœ… | โŒ | โŒ | โŒ | Limited | +| Mermaid import (8 types) | โœ… | โŒ | โš ๏ธ | โœ… | โŒ | +| Auto-icon assignment (1,600+) | โœ… | โŒ | โŒ | โŒ | โŒ | +| AWS / Azure / GCP / CNCF icons | โœ… | โŒ | โœ… | Partial | โœ… | | Real-time collaboration (P2P) `Beta` | โœ… | โœ… | โŒ | โŒ | โœ… (cloud) | -| Cinematic animated export | โœ… | โŒ | โŒ | โŒ | โŒ | -| Figma export (editable SVG) | โœ… | โŒ | โŒ | โŒ | โŒ | -| No account required | โœ… | โœ… | โœ… | โœ… | โŒ | -| Open source (MIT) | โœ… | โœ… | โœ… | โœ… | โŒ | +| Cinematic animated export | โœ… | โŒ | โŒ | โŒ | โŒ | +| Figma export (editable SVG) | โœ… | โŒ | โŒ | โŒ | โŒ | +| No account required | โœ… | โœ… | โœ… | โœ… | โŒ | +| Open source (MIT) | โœ… | โœ… | โœ… | โœ… | โŒ | --- -## Code โ†’ Diagram +## Paste Mermaid โ†’ Beautiful Diagrams -Drop in your existing artifacts. Many formats are handled by **deterministic native parsers** that run entirely in your browser. AI-powered imports help when the source needs interpretation or when you want a richer first-pass architecture draft. +Paste any Mermaid flowchart, architecture, state diagram, class diagram, ER diagram, sequence diagram, mindmap, or journey โ€” all 8 diagram families. OpenFlowKit renders it on a visual canvas and automatically assigns the correct branded icon to every technology node. -**Native parsers (no API key needed):** - -```sql -CREATE TABLE orders ( - id BIGINT PRIMARY KEY, - user_id BIGINT NOT NULL REFERENCES users(id), - status ENUM('pending','paid','shipped') NOT NULL -); +``` +flowchart TD + API[Express API] --> DB[(PostgreSQL)] + DB --> Cache[Redis Cache] + Cache --> Queue[RabbitMQ] ``` -โ†’ Typed ERD with inferred foreign-key edges and cardinalities. Rendered in milliseconds, no server involved. +Paste this โ†’ you get the Express wordmark, PostgreSQL elephant, Redis logo, and RabbitMQ icon โ€” all auto-detected, all beautifully laid out. No other tool does this. -```yaml -# deployment.yaml -apiVersion: apps/v1 -kind: Deployment -spec: - replicas: 3 ---- -apiVersion: v1 -kind: Service -selector: - app: api ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -spec: - rules: - - host: api.example.com -``` +**1,600+ icons** from developer, AWS, Azure, CNCF, and GCP catalogs are matched automatically based on node labels. No manual drag-and-drop. No configuration. + +### How it works + +1. **Paste Mermaid** on the canvas or in the code panel +2. **Semantic classifier** detects technology names (PostgreSQL, Redis, Express, Lambda, etc.) +3. **Icon matcher** searches 1,100+ icons across all catalogs โ€” exact match, then alias, then substring +4. **Enricher** assigns colors, icons, and provider SVGs to every node +5. **ELK layout** arranges everything cleanly + +### Mermaid quality gates + +- `npm run test:mermaid` runs the broad Mermaid parser/plugin/round-trip gate +- `npm run test:mermaid:layout` runs the layout, import-state, and recovery corpus gate +- `npm run test:mermaid:gold` runs both together -โ†’ Kubernetes architecture with Deployment โ†’ Service โ†’ Ingress connections. +### AI generation (API key required) -**AI-powered imports (API key required):** +Describe your system in plain English. AI generates a diagram on the canvas with correct icons applied automatically. -Paste source code, infrastructure, or API specs and hit generate โ€” the diagram lands directly on your canvas. AWS, Azure, GCP, and CNCF icons are automatically applied when the AI detects cloud services in your input. +| Prompt | Output | +| ----------------------------------------- | --------------------------------------- | +| "Node.js API with PostgreSQL and Redis" | 3 nodes with correct icons | +| "AWS Lambda โ†’ SQS โ†’ DynamoDB" | 3 nodes with AWS icons | +| "React frontend โ†’ Express โ†’ MongoDB โ†’ S3" | 4 nodes across developer + AWS catalogs | -| Source | Engine | API key? | -| ----------------------------------- | ------------------------- | :------: | -| SQL DDL | **Native parser** | **No** | -| Terraform `.tfstate` | **Native parser** | **No** | -| Terraform HCL | AI-assisted | Yes | -| Kubernetes YAML / Helm | **Native parser** | **No** | -| OpenAPI / Swagger YAML/JSON | **Native parser** | **No** | -| OpenAPI source text โ†’ richer flow | AI-assisted | Yes | -| Source code (single file) | AI-assisted ยท 9 languages | Yes | -| Mermaid | **Native parser** | **No** | +9 providers supported: Google Gemini, OpenAI, Anthropic Claude, Groq, Mistral, NVIDIA NIM, Cerebras, OpenRouter, or any custom OpenAI-compatible endpoint. --- @@ -194,17 +183,17 @@ Flowpilot sits directly in the editor. Describe a system, paste source code, upl **9 providers. Bring your own key. Switch any time.** -| Provider | Default model | Why use it | -| ------------------- | -------------------------------- | ----------------------------------------------- | -| Google Gemini | `gemini-2.5-flash-lite` | Free tier available, fast, browser-safe | -| OpenAI | `gpt-5-mini` | Best reasoning for complex architectures | -| Anthropic Claude | `claude-sonnet-4-6` | Excellent code and system understanding | -| Groq | `llama-4-scout-17b-16e-instruct` | Fastest inference available | -| Mistral | `mistral-medium-latest` | Strong European privacy-first alternative | -| NVIDIA NIM | `llama-4-scout-17b-16e-instruct` | Enterprise GPU inference | -| Cerebras | `gpt-oss-120b` | Fastest on WSE-3 silicon | -| OpenRouter | `google/gemini-2.5-flash` | Access 100+ models through one key | -| **Custom endpoint** | Any model | Ollama, LM Studio, or any OpenAI-compatible API | +| Provider | Default model | Why use it | +| ------------------- | ------------------------------------------ | ----------------------------------------------- | +| Google Gemini | `gemini-2.5-flash-lite` | Free tier available, fast, browser-safe | +| OpenAI | `gpt-5-mini` | Best reasoning for complex architectures | +| Anthropic Claude | `claude-sonnet-4-6` | Excellent code and system understanding | +| Groq | `meta-llama/llama-4-scout-17b-16e-instruct`| Fastest open-source inference available | +| Mistral | `mistral-large-latest` | Strong European privacy-first alternative | +| NVIDIA NIM | `meta/llama-4-maverick-17b-128e-instruct` | Enterprise GPU inference | +| Cerebras | `gpt-oss-120b` | Ultra-fast on WSE-3 silicon | +| OpenRouter | `google/gemini-2.5-pro` | Access 300+ models through one key | +| **Custom endpoint** | Any model | Ollama, LM Studio, or any OpenAI-compatible API | No proxy. No middleman. Direct browser-to-provider requests. @@ -224,9 +213,10 @@ flowchart TB auth --> db ``` -- Mermaid-compatible syntax +- Mermaid-compatible syntax โ€” paste any Mermaid and it renders with auto-assigned icons +- Specify icons directly: `{ archProvider: "developer", archResourceType: "database-postgresql" }` +- Auto-icon resolution: nodes are enriched with the correct branded icon based on their label - Export to Mermaid, PlantUML, or JSON -- Paste any Mermaid diagram and it renders immediately - Version snapshots โ€” restore any previous state --- @@ -267,7 +257,7 @@ Designed for architecture reviews, onboarding docs, and demos where a static ima > **No other open-source diagramming tool does this.** -Export as **WebM**, control animation speed, and share a link or embed it anywhere. +Export as **WebM or MP4** (browser-native, no codec install needed), control animation speed, and share or embed anywhere. --- @@ -275,7 +265,7 @@ Export as **WebM**, control animation speed, and share a link or embed it anywhe Build your diagram once. Take it anywhere. -- **๐ŸŽฌ Cinematic MP4 / GIF** โ€” animated walkthrough, browser-only, no upload required +- **๐ŸŽฌ Cinematic WebM / MP4** โ€” animated build walkthrough, browser-only, no upload required - **PNG / SVG** โ€” transparent background, pixel-perfect at any resolution - **PDF** โ€” print-ready, vector-crisp - **Mermaid** โ€” paste directly into GitHub READMEs, Notion, Confluence, Linear @@ -310,7 +300,7 @@ Plus: smart alignment guides, snap-to-grid, multi-select, pages, layers, section Current roadmap focus: -- **GIF export for cinematic animations** โ€” WebM is shipping now; GIF export coming next so diagrams can be embedded anywhere without conversion +- **GIF export for cinematic animations** โ€” WebM/MP4 ship today; GIF export for zero-conversion embeds is next - **GitHub repo โ†’ diagram** โ€” currently in beta internally; will ship when output quality is consistent across real-world codebases - better layers and page workflows for larger technical diagrams - stronger code and structured-import diagram quality diff --git a/assets/third-party-icons/developer/processed/Analytics/databricks.svg b/assets/third-party-icons/developer/processed/Analytics/databricks.svg new file mode 100644 index 00000000..d67b1705 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/databricks.svg @@ -0,0 +1 @@ +Databricks \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/flink.svg b/assets/third-party-icons/developer/processed/Analytics/flink.svg new file mode 100644 index 00000000..254b43f2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/flink.svg @@ -0,0 +1 @@ +Apache Flink \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/hadoop.svg b/assets/third-party-icons/developer/processed/Analytics/hadoop.svg new file mode 100644 index 00000000..a03f6844 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/hadoop.svg @@ -0,0 +1 @@ +Apache Hadoop \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/hive.svg b/assets/third-party-icons/developer/processed/Analytics/hive.svg new file mode 100644 index 00000000..a1b23393 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/hive.svg @@ -0,0 +1 @@ +Apache Hive \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/solr.svg b/assets/third-party-icons/developer/processed/Analytics/solr.svg new file mode 100644 index 00000000..75600aaa --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/solr.svg @@ -0,0 +1 @@ +Apache Solr \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/spark.svg b/assets/third-party-icons/developer/processed/Analytics/spark.svg new file mode 100644 index 00000000..106fc42e --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/spark.svg @@ -0,0 +1 @@ +Apache Spark \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/superset.svg b/assets/third-party-icons/developer/processed/Analytics/superset.svg new file mode 100644 index 00000000..735b4b42 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/superset.svg @@ -0,0 +1 @@ +Apache Superset \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/trino.svg b/assets/third-party-icons/developer/processed/Analytics/trino.svg new file mode 100644 index 00000000..2af274c8 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/trino.svg @@ -0,0 +1 @@ +Trino \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Backend/dapr.svg b/assets/third-party-icons/developer/processed/Backend/dapr.svg new file mode 100644 index 00000000..7ea3d977 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Backend/dapr.svg @@ -0,0 +1 @@ +Dapr \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Backend/dotnet.svg b/assets/third-party-icons/developer/processed/Backend/dotnet.svg new file mode 100644 index 00000000..c4b482ec --- /dev/null +++ b/assets/third-party-icons/developer/processed/Backend/dotnet.svg @@ -0,0 +1 @@ +.NET \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Backend/phoenix.svg b/assets/third-party-icons/developer/processed/Backend/phoenix.svg new file mode 100644 index 00000000..e216b465 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Backend/phoenix.svg @@ -0,0 +1 @@ +Phoenix Framework \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Backend/quarkus.svg b/assets/third-party-icons/developer/processed/Backend/quarkus.svg new file mode 100644 index 00000000..892f6f44 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Backend/quarkus.svg @@ -0,0 +1 @@ +Quarkus \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Container/containerd.svg b/assets/third-party-icons/developer/processed/Container/containerd.svg new file mode 100644 index 00000000..6e87c072 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Container/containerd.svg @@ -0,0 +1 @@ +containerd \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Container/k3s.svg b/assets/third-party-icons/developer/processed/Container/k3s.svg new file mode 100644 index 00000000..c4dbcbe3 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Container/k3s.svg @@ -0,0 +1 @@ +K3s \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Container/lxc.svg b/assets/third-party-icons/developer/processed/Container/lxc.svg new file mode 100644 index 00000000..08d405b4 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Container/lxc.svg @@ -0,0 +1 @@ +Linux Containers \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/cockroachdb.svg b/assets/third-party-icons/developer/processed/Database/cockroachdb.svg new file mode 100644 index 00000000..6929bf3f --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/cockroachdb.svg @@ -0,0 +1 @@ +Cockroach Labs \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/couchbase.svg b/assets/third-party-icons/developer/processed/Database/couchbase.svg new file mode 100644 index 00000000..6e57b7e3 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/couchbase.svg @@ -0,0 +1 @@ +Couchbase \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/couchdb.svg b/assets/third-party-icons/developer/processed/Database/couchdb.svg new file mode 100644 index 00000000..0f6297a9 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/couchdb.svg @@ -0,0 +1 @@ +Apache CouchDB \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/druid.svg b/assets/third-party-icons/developer/processed/Database/druid.svg new file mode 100644 index 00000000..a3af5cba --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/druid.svg @@ -0,0 +1 @@ +Apache Druid \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/duckdb.svg b/assets/third-party-icons/developer/processed/Database/duckdb.svg new file mode 100644 index 00000000..ac31e6f9 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/duckdb.svg @@ -0,0 +1 @@ +DuckDB \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/influxdb.svg b/assets/third-party-icons/developer/processed/Database/influxdb.svg new file mode 100644 index 00000000..9ad681b4 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/influxdb.svg @@ -0,0 +1 @@ +InfluxDB \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/neo4j.svg b/assets/third-party-icons/developer/processed/Database/neo4j.svg new file mode 100644 index 00000000..b4194ce8 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/neo4j.svg @@ -0,0 +1 @@ +Neo4j \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/scylladb.svg b/assets/third-party-icons/developer/processed/Database/scylladb.svg new file mode 100644 index 00000000..f6da258d --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/scylladb.svg @@ -0,0 +1 @@ +ScyllaDB \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/airflow.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/airflow.svg new file mode 100644 index 00000000..fd6c19c2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/airflow.svg @@ -0,0 +1 @@ +Apache Airflow \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/ansible.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/ansible.svg new file mode 100644 index 00000000..2a121a2a --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/ansible.svg @@ -0,0 +1 @@ +Ansible \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/argocd.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/argocd.svg new file mode 100644 index 00000000..34239b81 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/argocd.svg @@ -0,0 +1 @@ +Argo \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/bentoml.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/bentoml.svg new file mode 100644 index 00000000..232cc2fb --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/bentoml.svg @@ -0,0 +1 @@ +BentoML \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/ceph.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/ceph.svg new file mode 100644 index 00000000..36810803 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/ceph.svg @@ -0,0 +1 @@ +Ceph \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/chef.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/chef.svg new file mode 100644 index 00000000..f0fd1b8d --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/chef.svg @@ -0,0 +1 @@ +Chef \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/droneci.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/droneci.svg new file mode 100644 index 00000000..47b8f613 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/droneci.svg @@ -0,0 +1 @@ +Drone \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/flux.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/flux.svg new file mode 100644 index 00000000..7ba68c5e --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/flux.svg @@ -0,0 +1 @@ +Flux \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/harbor.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/harbor.svg new file mode 100644 index 00000000..8dc9fa9d --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/harbor.svg @@ -0,0 +1 @@ +Harbor \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/jfrog.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/jfrog.svg new file mode 100644 index 00000000..1f7aae01 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/jfrog.svg @@ -0,0 +1 @@ +JFrog \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/keycloak.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/keycloak.svg new file mode 100644 index 00000000..a2c6a28d --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/keycloak.svg @@ -0,0 +1 @@ +Keycloak \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/minio.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/minio.svg new file mode 100644 index 00000000..12f8ffdd --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/minio.svg @@ -0,0 +1 @@ +MinIO \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/mlflow.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/mlflow.svg new file mode 100644 index 00000000..8d1b11f4 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/mlflow.svg @@ -0,0 +1 @@ +MLflow \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/nomad.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/nomad.svg new file mode 100644 index 00000000..25dae0d2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/nomad.svg @@ -0,0 +1 @@ +Nomad \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/oauth2.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/oauth2.svg new file mode 100644 index 00000000..32a6dc3f --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/oauth2.svg @@ -0,0 +1 @@ +Auth0 \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/prefect.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/prefect.svg new file mode 100644 index 00000000..718c735a --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/prefect.svg @@ -0,0 +1 @@ +Prefect \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/puppet.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/puppet.svg new file mode 100644 index 00000000..313bf0d9 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/puppet.svg @@ -0,0 +1 @@ +Puppet \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/teamcity.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/teamcity.svg new file mode 100644 index 00000000..3a481926 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/teamcity.svg @@ -0,0 +1 @@ +TeamCity \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/temporal.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/temporal.svg new file mode 100644 index 00000000..f815f900 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/temporal.svg @@ -0,0 +1 @@ +Temporal \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/travisci.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/travisci.svg new file mode 100644 index 00000000..723a1b3b --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/travisci.svg @@ -0,0 +1 @@ +Travis CI \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/vault.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/vault.svg new file mode 100644 index 00000000..adfcbdb0 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/vault.svg @@ -0,0 +1 @@ +Vault \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/wandb.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/wandb.svg new file mode 100644 index 00000000..b2f5d554 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/wandb.svg @@ -0,0 +1 @@ +Weights & Biases \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/analytics.svg b/assets/third-party-icons/developer/processed/GCP/analytics.svg new file mode 100644 index 00000000..8b68c100 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/analytics.svg @@ -0,0 +1 @@ +Google Analytics \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/bigquery.svg b/assets/third-party-icons/developer/processed/GCP/bigquery.svg new file mode 100644 index 00000000..aa40433b --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/bigquery.svg @@ -0,0 +1 @@ +Google BigQuery \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/cloud-composer.svg b/assets/third-party-icons/developer/processed/GCP/cloud-composer.svg new file mode 100644 index 00000000..4ff2211e --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/cloud-composer.svg @@ -0,0 +1 @@ +Google Cloud Composer \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/cloud-spanner.svg b/assets/third-party-icons/developer/processed/GCP/cloud-spanner.svg new file mode 100644 index 00000000..d3ed6610 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/cloud-spanner.svg @@ -0,0 +1 @@ +Google Cloud Spanner \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/cloud-storage.svg b/assets/third-party-icons/developer/processed/GCP/cloud-storage.svg new file mode 100644 index 00000000..90978b76 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/cloud-storage.svg @@ -0,0 +1 @@ +Google Cloud Storage \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/colab.svg b/assets/third-party-icons/developer/processed/GCP/colab.svg new file mode 100644 index 00000000..434396ea --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/colab.svg @@ -0,0 +1 @@ +Google Colab \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/dataflow.svg b/assets/third-party-icons/developer/processed/GCP/dataflow.svg new file mode 100644 index 00000000..46b1e5f7 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/dataflow.svg @@ -0,0 +1 @@ +Google Dataflow \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/dataproc.svg b/assets/third-party-icons/developer/processed/GCP/dataproc.svg new file mode 100644 index 00000000..1ec18f35 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/dataproc.svg @@ -0,0 +1 @@ +Google Dataproc \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/docs.svg b/assets/third-party-icons/developer/processed/GCP/docs.svg new file mode 100644 index 00000000..12077819 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/docs.svg @@ -0,0 +1 @@ +Google Docs \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/drive.svg b/assets/third-party-icons/developer/processed/GCP/drive.svg new file mode 100644 index 00000000..7263ef31 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/drive.svg @@ -0,0 +1 @@ +Google Drive \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/gemini.svg b/assets/third-party-icons/developer/processed/GCP/gemini.svg new file mode 100644 index 00000000..e15e53ce --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/gemini.svg @@ -0,0 +1 @@ +Google Gemini \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/google-ads.svg b/assets/third-party-icons/developer/processed/GCP/google-ads.svg new file mode 100644 index 00000000..d0ad5413 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/google-ads.svg @@ -0,0 +1 @@ +Google Ads \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/google-cloud.svg b/assets/third-party-icons/developer/processed/GCP/google-cloud.svg new file mode 100644 index 00000000..9c069578 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/google-cloud.svg @@ -0,0 +1 @@ +Google Cloud \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/looker.svg b/assets/third-party-icons/developer/processed/GCP/looker.svg new file mode 100644 index 00000000..31db1e5c --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/looker.svg @@ -0,0 +1 @@ +Looker \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/maps.svg b/assets/third-party-icons/developer/processed/GCP/maps.svg new file mode 100644 index 00000000..2c928cd7 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/maps.svg @@ -0,0 +1 @@ +Google Maps \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/pubsub.svg b/assets/third-party-icons/developer/processed/GCP/pubsub.svg new file mode 100644 index 00000000..1e39cc62 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/pubsub.svg @@ -0,0 +1 @@ +Google Pub/Sub \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/sheets.svg b/assets/third-party-icons/developer/processed/GCP/sheets.svg new file mode 100644 index 00000000..3dce7189 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/sheets.svg @@ -0,0 +1 @@ +Google Sheets \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/slides.svg b/assets/third-party-icons/developer/processed/GCP/slides.svg new file mode 100644 index 00000000..398312b2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/slides.svg @@ -0,0 +1 @@ +Google Slides \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/caddy.svg b/assets/third-party-icons/developer/processed/Infra/caddy.svg new file mode 100644 index 00000000..c9cd0a5e --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/caddy.svg @@ -0,0 +1 @@ +Caddy \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/consul.svg b/assets/third-party-icons/developer/processed/Infra/consul.svg new file mode 100644 index 00000000..9d494e06 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/consul.svg @@ -0,0 +1 @@ +Consul \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/envoy.svg b/assets/third-party-icons/developer/processed/Infra/envoy.svg new file mode 100644 index 00000000..c60a821d --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/envoy.svg @@ -0,0 +1 @@ +Envoy Proxy \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/etcd.svg b/assets/third-party-icons/developer/processed/Infra/etcd.svg new file mode 100644 index 00000000..cb9f3cee --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/etcd.svg @@ -0,0 +1 @@ +etcd \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/gunicorn.svg b/assets/third-party-icons/developer/processed/Infra/gunicorn.svg new file mode 100644 index 00000000..11396b80 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/gunicorn.svg @@ -0,0 +1 @@ +Gunicorn \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/istio.svg b/assets/third-party-icons/developer/processed/Infra/istio.svg new file mode 100644 index 00000000..580b9fd5 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/istio.svg @@ -0,0 +1 @@ +Istio \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/kong.svg b/assets/third-party-icons/developer/processed/Infra/kong.svg new file mode 100644 index 00000000..d323c3a4 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/kong.svg @@ -0,0 +1 @@ +Kong \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/letsencrypt.svg b/assets/third-party-icons/developer/processed/Infra/letsencrypt.svg new file mode 100644 index 00000000..c4bdcf6d --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/letsencrypt.svg @@ -0,0 +1 @@ +Let's Encrypt \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/linkerd.svg b/assets/third-party-icons/developer/processed/Infra/linkerd.svg new file mode 100644 index 00000000..512d0d82 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/linkerd.svg @@ -0,0 +1 @@ +Linkerd \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/nginx.svg b/assets/third-party-icons/developer/processed/Infra/nginx.svg new file mode 100644 index 00000000..e52ea9fe --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/nginx.svg @@ -0,0 +1 @@ +NGINX \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/tomcat.svg b/assets/third-party-icons/developer/processed/Infra/tomcat.svg new file mode 100644 index 00000000..4de01fca --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/tomcat.svg @@ -0,0 +1 @@ +Apache Tomcat \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/traefik.svg b/assets/third-party-icons/developer/processed/Infra/traefik.svg new file mode 100644 index 00000000..0135fda5 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/traefik.svg @@ -0,0 +1 @@ +Traefik Proxy \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/cpp.svg b/assets/third-party-icons/developer/processed/Languages/cpp.svg new file mode 100644 index 00000000..3d1b49e2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/cpp.svg @@ -0,0 +1 @@ +C++ \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/dart.svg b/assets/third-party-icons/developer/processed/Languages/dart.svg new file mode 100644 index 00000000..990da555 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/dart.svg @@ -0,0 +1 @@ +Dart \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/latex.svg b/assets/third-party-icons/developer/processed/Languages/latex.svg new file mode 100644 index 00000000..5bab7a8b --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/latex.svg @@ -0,0 +1 @@ +LaTeX \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/lua.svg b/assets/third-party-icons/developer/processed/Languages/lua.svg new file mode 100644 index 00000000..5f4c6521 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/lua.svg @@ -0,0 +1 @@ +Lua \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/zig.svg b/assets/third-party-icons/developer/processed/Languages/zig.svg new file mode 100644 index 00000000..d8504941 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/zig.svg @@ -0,0 +1 @@ +Zig \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Logging/fluentbit.svg b/assets/third-party-icons/developer/processed/Logging/fluentbit.svg new file mode 100644 index 00000000..cc0c5ffc --- /dev/null +++ b/assets/third-party-icons/developer/processed/Logging/fluentbit.svg @@ -0,0 +1 @@ +Fluent Bit \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Logging/fluentd.svg b/assets/third-party-icons/developer/processed/Logging/fluentd.svg new file mode 100644 index 00000000..38d9169d --- /dev/null +++ b/assets/third-party-icons/developer/processed/Logging/fluentd.svg @@ -0,0 +1 @@ +Fluentd \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Logging/graylog.svg b/assets/third-party-icons/developer/processed/Logging/graylog.svg new file mode 100644 index 00000000..d49fc873 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Logging/graylog.svg @@ -0,0 +1 @@ +Graylog \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/datadog.svg b/assets/third-party-icons/developer/processed/Monitoring/datadog.svg new file mode 100644 index 00000000..ab437319 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/datadog.svg @@ -0,0 +1 @@ +Datadog \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/dynatrace.svg b/assets/third-party-icons/developer/processed/Monitoring/dynatrace.svg new file mode 100644 index 00000000..d83ff94f --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/dynatrace.svg @@ -0,0 +1 @@ +Dynatrace \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/jaeger.svg b/assets/third-party-icons/developer/processed/Monitoring/jaeger.svg new file mode 100644 index 00000000..74155a60 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/jaeger.svg @@ -0,0 +1 @@ +Jaeger \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/newrelic.svg b/assets/third-party-icons/developer/processed/Monitoring/newrelic.svg new file mode 100644 index 00000000..f535b78b --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/newrelic.svg @@ -0,0 +1 @@ +New Relic \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/prometheus.svg b/assets/third-party-icons/developer/processed/Monitoring/prometheus.svg new file mode 100644 index 00000000..7dd2b46f --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/prometheus.svg @@ -0,0 +1 @@ +Prometheus \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/sentry.svg b/assets/third-party-icons/developer/processed/Monitoring/sentry.svg new file mode 100644 index 00000000..47888975 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/sentry.svg @@ -0,0 +1 @@ +Sentry \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/splunk.svg b/assets/third-party-icons/developer/processed/Monitoring/splunk.svg new file mode 100644 index 00000000..e84d30db --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/splunk.svg @@ -0,0 +1 @@ +Splunk \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Queue/celery.svg b/assets/third-party-icons/developer/processed/Queue/celery.svg new file mode 100644 index 00000000..9dd7df66 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Queue/celery.svg @@ -0,0 +1 @@ +Celery \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Queue/nats.svg b/assets/third-party-icons/developer/processed/Queue/nats.svg new file mode 100644 index 00000000..1c31020c --- /dev/null +++ b/assets/third-party-icons/developer/processed/Queue/nats.svg @@ -0,0 +1 @@ +NATS.io \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Queue/rabbitmq.svg b/assets/third-party-icons/developer/processed/Queue/rabbitmq.svg new file mode 100644 index 00000000..f8b90f5b --- /dev/null +++ b/assets/third-party-icons/developer/processed/Queue/rabbitmq.svg @@ -0,0 +1 @@ +RabbitMQ \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json b/assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json index 69314059..a7f69957 100644 --- a/assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json +++ b/assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json @@ -3193,6 +3193,936 @@ "defaultHeight": 96, "nodeType": "custom", "defaultData": {} + }, + { + "id": "infra-nginx", + "label": "Nginx", + "category": "Infra", + "svgContent": "NGINX", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-traefik", + "label": "Traefik", + "category": "Infra", + "svgContent": "Traefik Proxy", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-envoy", + "label": "Envoy", + "category": "Infra", + "svgContent": "Envoy Proxy", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-istio", + "label": "Istio", + "category": "Infra", + "svgContent": "Istio", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-consul", + "label": "Consul", + "category": "Infra", + "svgContent": "Consul", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-kong", + "label": "Kong", + "category": "Infra", + "svgContent": "Kong", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-etcd", + "label": "etcd", + "category": "Infra", + "svgContent": "etcd", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-linkerd", + "label": "Linkerd", + "category": "Infra", + "svgContent": "Linkerd", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-caddy", + "label": "Caddy", + "category": "Infra", + "svgContent": "Caddy", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-gunicorn", + "label": "Gunicorn", + "category": "Infra", + "svgContent": "Gunicorn", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-tomcat", + "label": "Tomcat", + "category": "Infra", + "svgContent": "Apache Tomcat", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-prometheus", + "label": "Prometheus", + "category": "Monitoring", + "svgContent": "Prometheus", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-sentry", + "label": "Sentry", + "category": "Monitoring", + "svgContent": "Sentry", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-newrelic", + "label": "New Relic", + "category": "Monitoring", + "svgContent": "New Relic", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-datadog", + "label": "Datadog", + "category": "Monitoring", + "svgContent": "Datadog", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-dynatrace", + "label": "Dynatrace", + "category": "Monitoring", + "svgContent": "Dynatrace", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-splunk", + "label": "Splunk", + "category": "Monitoring", + "svgContent": "Splunk", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-influxdb", + "label": "InfluxDB", + "category": "Database", + "svgContent": "InfluxDB", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-cockroachdb", + "label": "CockroachDB", + "category": "Database", + "svgContent": "Cockroach Labs", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-neo4j", + "label": "Neo4j", + "category": "Database", + "svgContent": "Neo4j", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-couchdb", + "label": "CouchDB", + "category": "Database", + "svgContent": "Apache CouchDB", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-couchbase", + "label": "Couchbase", + "category": "Database", + "svgContent": "Couchbase", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-duckdb", + "label": "DuckDB", + "category": "Database", + "svgContent": "DuckDB", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-scylla", + "label": "ScyllaDB", + "category": "Database", + "svgContent": "ScyllaDB", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-druid", + "label": "Apache Druid", + "category": "Database", + "svgContent": "Apache Druid", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "queue-rabbitmq", + "label": "RabbitMQ", + "category": "Queue", + "svgContent": "RabbitMQ", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "queue-celery", + "label": "Celery", + "category": "Queue", + "svgContent": "Celery", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "container-containerd", + "label": "containerd", + "category": "Container", + "svgContent": "containerd", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "container-k3s", + "label": "K3s", + "category": "Container", + "svgContent": "K3s", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "container-lxc", + "label": "LXC", + "category": "Container", + "svgContent": "Linux Containers", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "logging-fluentbit", + "label": "Fluent Bit", + "category": "Logging", + "svgContent": "Fluent Bit", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "logging-fluentd", + "label": "Fluentd", + "category": "Logging", + "svgContent": "Fluentd", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "logging-graylog", + "label": "Graylog", + "category": "Logging", + "svgContent": "Graylog", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ci-travisci", + "label": "Travis CI", + "category": "CI-CD", + "svgContent": "Travis CI", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ci-teamcity", + "label": "TeamCity", + "category": "CI-CD", + "svgContent": "TeamCity", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ci-droneci", + "label": "Drone CI", + "category": "CI-CD", + "svgContent": "Drone", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gitops-argocd", + "label": "Argo CD", + "category": "GitOps", + "svgContent": "Argo", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gitops-flux", + "label": "Flux", + "category": "GitOps", + "svgContent": "Flux", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "iac-puppet", + "label": "Puppet", + "category": "IaC", + "svgContent": "Puppet", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "iac-ansible", + "label": "Ansible", + "category": "IaC", + "svgContent": "Ansible", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "iac-chef", + "label": "Chef", + "category": "IaC", + "svgContent": "Chef", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "storage-ceph", + "label": "Ceph", + "category": "Storage", + "svgContent": "Ceph", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "storage-minio", + "label": "MinIO", + "category": "Storage", + "svgContent": "MinIO", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "auth-vault", + "label": "Vault", + "category": "Security", + "svgContent": "Vault", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "auth-keycloak", + "label": "Keycloak", + "category": "Security", + "svgContent": "Keycloak", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "auth-oauth2", + "label": "OAuth 2.0", + "category": "Security", + "svgContent": "Auth0", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-letsencrypt", + "label": "Let's Encrypt", + "category": "Infra", + "svgContent": "Let's Encrypt", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "workflow-airflow", + "label": "Apache Airflow", + "category": "Workflow", + "svgContent": "Apache Airflow", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "workflow-prefect", + "label": "Prefect", + "category": "Workflow", + "svgContent": "Prefect", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "workflow-temporal", + "label": "Temporal", + "category": "Workflow", + "svgContent": "Temporal", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "registry-harbor", + "label": "Harbor", + "category": "Registry", + "svgContent": "Harbor", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "registry-jfrog", + "label": "JFrog", + "category": "Registry", + "svgContent": "JFrog", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-spark", + "label": "Apache Spark", + "category": "Analytics", + "svgContent": "Apache Spark", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-flink", + "label": "Apache Flink", + "category": "Analytics", + "svgContent": "Apache Flink", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-hadoop", + "label": "Hadoop", + "category": "Analytics", + "svgContent": "Apache Hadoop", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-hive", + "label": "Apache Hive", + "category": "Analytics", + "svgContent": "Apache Hive", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-databricks", + "label": "Databricks", + "category": "Analytics", + "svgContent": "Databricks", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-superset", + "label": "Superset", + "category": "Analytics", + "svgContent": "Apache Superset", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-trino", + "label": "Trino", + "category": "Analytics", + "svgContent": "Trino", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-solr", + "label": "Apache Solr", + "category": "Analytics", + "svgContent": "Apache Solr", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ml-mlflow", + "label": "MLflow", + "category": "ML", + "svgContent": "MLflow", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ml-wandb", + "label": "Weights & Biases", + "category": "ML", + "svgContent": "Weights & Biases", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ml-bentoml", + "label": "BentoML", + "category": "ML", + "svgContent": "BentoML", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "backend-dotnet", + "label": ".NET", + "category": "Backend", + "svgContent": ".NET", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "backend-quarkus", + "label": "Quarkus", + "category": "Backend", + "svgContent": "Quarkus", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "backend-phoenix", + "label": "Phoenix", + "category": "Backend", + "svgContent": "Phoenix Framework", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "backend-dapr", + "label": "Dapr", + "category": "Backend", + "svgContent": "Dapr", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-cpp", + "label": "C++", + "category": "Languages", + "svgContent": "C++", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-latex", + "label": "LaTeX", + "category": "Languages", + "svgContent": "LaTeX", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-zig", + "label": "Zig", + "category": "Languages", + "svgContent": "Zig", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-lua", + "label": "Lua", + "category": "Languages", + "svgContent": "Lua", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-dart", + "label": "Dart", + "category": "Languages", + "svgContent": "Dart", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-jaeger", + "label": "Jaeger", + "category": "Monitoring", + "svgContent": "Jaeger", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "queue-nats", + "label": "NATS", + "category": "Queue", + "svgContent": "NATS.io", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "iac-nomad", + "label": "Nomad", + "category": "IaC", + "svgContent": "Nomad", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-googlecloud", + "label": "Google Cloud", + "category": "GCP", + "svgContent": "Google Cloud", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-bigquery", + "label": "BigQuery", + "category": "GCP", + "svgContent": "Google BigQuery", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-cloudstorage", + "label": "Cloud Storage", + "category": "GCP", + "svgContent": "Google Cloud Storage", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-pubsub", + "label": "Cloud Pub/Sub", + "category": "GCP", + "svgContent": "Google Pub/Sub", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-spanner", + "label": "Cloud Spanner", + "category": "GCP", + "svgContent": "Google Cloud Spanner", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-composer", + "label": "Cloud Composer", + "category": "GCP", + "svgContent": "Google Cloud Composer", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-dataflow", + "label": "Dataflow", + "category": "GCP", + "svgContent": "Google Dataflow", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-dataproc", + "label": "Dataproc", + "category": "GCP", + "svgContent": "Google Dataproc", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-gemini", + "label": "Gemini", + "category": "GCP", + "svgContent": "Google Gemini", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-colab", + "label": "Google Colab", + "category": "GCP", + "svgContent": "Google Colab", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-maps", + "label": "Google Maps", + "category": "GCP", + "svgContent": "Google Maps", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-analytics", + "label": "Google Analytics", + "category": "GCP", + "svgContent": "Google Analytics", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-ads", + "label": "Google Ads", + "category": "GCP", + "svgContent": "Google Ads", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-drive", + "label": "Google Drive", + "category": "GCP", + "svgContent": "Google Drive", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-docs", + "label": "Google Docs", + "category": "GCP", + "svgContent": "Google Docs", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-sheets", + "label": "Google Sheets", + "category": "GCP", + "svgContent": "Google Sheets", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-slides", + "label": "Google Slides", + "category": "GCP", + "svgContent": "Google Slides", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-looker", + "label": "Looker", + "category": "GCP", + "svgContent": "Looker", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} } ] -} +} \ No newline at end of file diff --git a/assets/third-party-icons/google-cloud/SOURCE.md b/assets/third-party-icons/gcp/SOURCE.md similarity index 100% rename from assets/third-party-icons/google-cloud/SOURCE.md rename to assets/third-party-icons/gcp/SOURCE.md diff --git a/assets/third-party-icons/google-cloud/processed/.gitkeep b/assets/third-party-icons/gcp/processed/.gitkeep similarity index 100% rename from assets/third-party-icons/google-cloud/processed/.gitkeep rename to assets/third-party-icons/gcp/processed/.gitkeep diff --git a/assets/third-party-icons/gcp/processed/Core/analytics.svg b/assets/third-party-icons/gcp/processed/Core/analytics.svg new file mode 100644 index 00000000..8b68c100 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/analytics.svg @@ -0,0 +1 @@ +Google Analytics \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/bigquery.svg b/assets/third-party-icons/gcp/processed/Core/bigquery.svg new file mode 100644 index 00000000..aa40433b --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/bigquery.svg @@ -0,0 +1 @@ +Google BigQuery \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/cloud-composer.svg b/assets/third-party-icons/gcp/processed/Core/cloud-composer.svg new file mode 100644 index 00000000..4ff2211e --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/cloud-composer.svg @@ -0,0 +1 @@ +Google Cloud Composer \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/cloud-spanner.svg b/assets/third-party-icons/gcp/processed/Core/cloud-spanner.svg new file mode 100644 index 00000000..d3ed6610 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/cloud-spanner.svg @@ -0,0 +1 @@ +Google Cloud Spanner \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/cloud-storage.svg b/assets/third-party-icons/gcp/processed/Core/cloud-storage.svg new file mode 100644 index 00000000..90978b76 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/cloud-storage.svg @@ -0,0 +1 @@ +Google Cloud Storage \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/colab.svg b/assets/third-party-icons/gcp/processed/Core/colab.svg new file mode 100644 index 00000000..434396ea --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/colab.svg @@ -0,0 +1 @@ +Google Colab \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/dataflow.svg b/assets/third-party-icons/gcp/processed/Core/dataflow.svg new file mode 100644 index 00000000..46b1e5f7 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/dataflow.svg @@ -0,0 +1 @@ +Google Dataflow \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/dataproc.svg b/assets/third-party-icons/gcp/processed/Core/dataproc.svg new file mode 100644 index 00000000..1ec18f35 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/dataproc.svg @@ -0,0 +1 @@ +Google Dataproc \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/docs.svg b/assets/third-party-icons/gcp/processed/Core/docs.svg new file mode 100644 index 00000000..12077819 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/docs.svg @@ -0,0 +1 @@ +Google Docs \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/drive.svg b/assets/third-party-icons/gcp/processed/Core/drive.svg new file mode 100644 index 00000000..7263ef31 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/drive.svg @@ -0,0 +1 @@ +Google Drive \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/gemini.svg b/assets/third-party-icons/gcp/processed/Core/gemini.svg new file mode 100644 index 00000000..e15e53ce --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/gemini.svg @@ -0,0 +1 @@ +Google Gemini \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/google-ads.svg b/assets/third-party-icons/gcp/processed/Core/google-ads.svg new file mode 100644 index 00000000..d0ad5413 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/google-ads.svg @@ -0,0 +1 @@ +Google Ads \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/google-cloud.svg b/assets/third-party-icons/gcp/processed/Core/google-cloud.svg new file mode 100644 index 00000000..9c069578 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/google-cloud.svg @@ -0,0 +1 @@ +Google Cloud \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/looker.svg b/assets/third-party-icons/gcp/processed/Core/looker.svg new file mode 100644 index 00000000..31db1e5c --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/looker.svg @@ -0,0 +1 @@ +Looker \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/maps.svg b/assets/third-party-icons/gcp/processed/Core/maps.svg new file mode 100644 index 00000000..2c928cd7 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/maps.svg @@ -0,0 +1 @@ +Google Maps \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/pubsub.svg b/assets/third-party-icons/gcp/processed/Core/pubsub.svg new file mode 100644 index 00000000..1e39cc62 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/pubsub.svg @@ -0,0 +1 @@ +Google Pub/Sub \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/sheets.svg b/assets/third-party-icons/gcp/processed/Core/sheets.svg new file mode 100644 index 00000000..3dce7189 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/sheets.svg @@ -0,0 +1 @@ +Google Sheets \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/slides.svg b/assets/third-party-icons/gcp/processed/Core/slides.svg new file mode 100644 index 00000000..398312b2 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/slides.svg @@ -0,0 +1 @@ +Google Slides \ No newline at end of file diff --git a/assets/third-party-icons/google-cloud/raw/.gitkeep b/assets/third-party-icons/gcp/raw/.gitkeep similarity index 100% rename from assets/third-party-icons/google-cloud/raw/.gitkeep rename to assets/third-party-icons/gcp/raw/.gitkeep diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index c9baa040..1e8849c8 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -12,11 +12,16 @@ async function openHomeDashboard(page: import('@playwright/test').Page) { await expect(page.getByTestId('home-create-new-main')).toBeVisible({ timeout: 15000 }); } -test('creates a new flow and adds an extra tab', async ({ page }) => { +async function createNewFlow(page: import('@playwright/test').Page) { await openHomeDashboard(page); await page.getByTestId('home-create-new-main').click(); - await expect(page).toHaveURL(/#\/flow\/[^?]+(?:\?.*)?$/); + await expect(page.getByTestId('flow-page-tab').first()).toBeVisible({ timeout: 15000 }); + await expect(page.getByTestId('topnav-menu-toggle')).toBeVisible({ timeout: 15000 }); +} + +test('creates a new flow and adds an extra tab', async ({ page }) => { + await createNewFlow(page); await expect(page.getByTestId('toolbar-add-toggle')).toBeVisible({ timeout: 15000 }); const tabs = page.getByTestId('flow-page-tab'); @@ -30,10 +35,8 @@ test('creates a new flow and adds an extra tab', async ({ page }) => { }); test('saves and restores snapshot state', async ({ page }) => { - await openHomeDashboard(page); - await page.getByTestId('home-create-new-main').click(); + await createNewFlow(page); await expect(page.getByTestId('toolbar-add-toggle')).toBeVisible({ timeout: 15000 }); - await expect(page.getByTestId('topnav-menu-toggle')).toBeVisible({ timeout: 15000 }); const canvasNodes = page.locator('.react-flow__node'); diff --git a/e2e/workflows.spec.ts b/e2e/workflows.spec.ts index 443972a6..95b7dcbe 100644 --- a/e2e/workflows.spec.ts +++ b/e2e/workflows.spec.ts @@ -16,11 +16,12 @@ async function createNewFlow(page: import('@playwright/test').Page) { await expect(page.getByTestId('home-create-new-main')).toBeVisible({ timeout: 15000 }); await page.getByTestId('home-create-new-main').click(); await expect(page).toHaveURL(/#\/flow\/[^?]+(?:\?.*)?$/); - await expect(page.getByTestId('toolbar-add-toggle')).toBeVisible({ timeout: 15000 }); await expect(page.getByTestId('flow-page-tab').first()).toBeVisible(); + await expect(page.getByTestId('topnav-menu-toggle')).toBeVisible({ timeout: 15000 }); } async function addRectangleNode(page: import('@playwright/test').Page) { + await expect(page.getByTestId('toolbar-add-toggle')).toBeVisible({ timeout: 15000 }); await page.getByTestId('toolbar-add-toggle').click(); await page.getByRole('button', { name: 'Rectangle' }).click(); } diff --git a/package-lock.json b/package-lock.json index f9fc3e50..cc33099b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,9 @@ ], "dependencies": { "@google/genai": "^1.40.0", - "@types/react-syntax-highlighter": "^15.5.13", + "@mermaid-js/layout-elk": "^0.2.1", "@xyflow/react": "^12.10.1", + "dagre": "^0.8.5", "elkjs": "^0.11.0", "framer-motion": "^12.34.0", "html-to-image": "^1.11.13", @@ -23,6 +24,7 @@ "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", "lucide-react": "^0.563.0", + "mermaid": "^11.14.0", "posthog-js": "^1.347.2", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -35,7 +37,6 @@ "remark-gfm": "4.0.0", "y-indexeddb": "^9.0.12", "y-webrtc": "^10.3.0", - "y-websocket": "^3.0.0", "yjs": "^13.6.29", "zustand": "^5.0.11" }, @@ -44,7 +45,9 @@ "@tailwindcss/postcss": "^4.1.18", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@types/dagre": "^0.7.54", "@types/node": "^22.14.0", + "@types/react-syntax-highlighter": "^15.5.13", "@typescript-eslint/eslint-plugin": "^8.55.0", "@typescript-eslint/parser": "^8.55.0", "@vitejs/plugin-react": "^5.1.4", @@ -112,6 +115,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", @@ -734,6 +750,12 @@ "node": ">=18" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, "node_modules/@capsizecss/unpack": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", @@ -746,6 +768,43 @@ "node": ">=18" } }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", + "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "12.0.0", + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/gast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", + "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", + "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", + "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", + "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", + "license": "Apache-2.0" + }, "node_modules/@csstools/color-helpers": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", @@ -1547,6 +1606,23 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -2168,6 +2244,34 @@ "node": ">= 12" } }, + "node_modules/@mermaid-js/layout-elk": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/layout-elk/-/layout-elk-0.2.1.tgz", + "integrity": "sha512-MX9jwhMyd5zDcFsYcl3duDUkKhjVRUCGEQrdCeNV5hCIR6+3FuDDbRbFmvVbAu15K1+juzsYGG+K8MDvCY1Amg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "elkjs": "^0.9.3" + }, + "peerDependencies": { + "mermaid": "^11.0.2" + } + }, + "node_modules/@mermaid-js/layout-elk/node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" + }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", + "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", + "license": "MIT", + "dependencies": { + "langium": "^4.0.0" + } + }, "node_modules/@microsoft/api-extractor": { "version": "7.43.0", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.0.tgz", @@ -4415,12 +4519,102 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, "node_modules/@types/d3-drag": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", @@ -4430,6 +4624,54 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", @@ -4439,12 +4681,78 @@ "@types/d3-color": "*" } }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, "node_modules/@types/d3-selection": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "license": "MIT" }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/d3-transition": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", @@ -4464,6 +4772,13 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/dagre": { + "version": "0.7.54", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.54.tgz", + "integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4495,6 +4810,12 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -4578,6 +4899,7 @@ "version": "15.5.13", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -4867,6 +5189,16 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", @@ -5202,9 +5534,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -6734,6 +7066,34 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chevrotain": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", + "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "12.0.0", + "@chevrotain/gast": "12.0.0", + "@chevrotain/regexp-to-ast": "12.0.0", + "@chevrotain/types": "12.0.0", + "@chevrotain/utils": "12.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz", + "integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^12.0.0" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -6944,6 +7304,12 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6986,6 +7352,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -7176,78 +7551,435 @@ "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^4.1.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.21", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.4" + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", + "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" }, "engines": { - "node": ">=20" + "node": ">=12" } }, - "node_modules/cssstyle/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", "engines": { - "node": "20 || >=22" + "node": ">=12" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/d3-dispatch": { + "node_modules/d3-quadtree": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, "engines": { "node": ">=12" } }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, "engines": { "node": ">=12" } }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "license": "ISC", "dependencies": { - "d3-color": "1 - 3" + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" }, "engines": { "node": ">=12" @@ -7262,6 +7994,42 @@ "node": ">=12" } }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-timer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", @@ -7306,6 +8074,26 @@ "node": ">=12" } }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -7383,6 +8171,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -7476,6 +8270,15 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -9422,6 +10225,15 @@ "dev": true, "license": "MIT" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/gtoken": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", @@ -9452,6 +10264,12 @@ "uncrypto": "^0.1.3" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -10162,6 +10980,18 @@ "cross-fetch": "4.0.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -10294,6 +11124,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -11195,6 +12034,31 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/katex": { + "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -11205,6 +12069,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -11230,6 +12099,30 @@ "dev": true, "license": "MIT" }, + "node_modules/langium": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz", + "integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==", + "license": "MIT", + "dependencies": { + "@chevrotain/regexp-to-ast": "~12.0.0", + "chevrotain": "~12.0.0", + "chevrotain-allstar": "~0.4.1", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.1.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11655,7 +12548,12 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.get": { @@ -11906,6 +12804,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12276,6 +13186,35 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", + "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "lodash-es": "^4.17.23", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -13099,6 +14038,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, "node_modules/motion-dom": { "version": "12.34.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.0.tgz", @@ -13690,6 +14641,12 @@ "dev": true, "license": "MIT" }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -13762,7 +14719,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, "license": "MIT" }, "node_modules/piccolore": { @@ -13815,6 +14771,17 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/playwright": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", @@ -13862,6 +14829,22 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -14977,6 +15960,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -15021,6 +16010,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -15045,6 +16046,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -15120,6 +16127,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", @@ -15904,6 +16917,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -16198,6 +17217,15 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tsconfck": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", @@ -16730,6 +17758,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/validator": { "version": "13.15.26", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", @@ -17001,6 +18042,55 @@ "node": ">=0.10.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, "node_modules/vue-template-compiler": { "version": "2.7.16", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", @@ -17503,27 +18593,6 @@ "yjs": "^13.6.8" } }, - "node_modules/y-websocket": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-3.0.0.tgz", - "integrity": "sha512-mUHy7AzkOZ834T/7piqtlA8Yk6AchqKqcrCXjKW8J1w2lPtRDjz8W5/CvXz9higKAHgKRKqpI3T33YkRFLkPtg==", - "license": "MIT", - "dependencies": { - "lib0": "^0.2.102", - "y-protocols": "^1.0.5" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors โค", - "url": "https://github.com/sponsors/dmonad" - }, - "peerDependencies": { - "yjs": "^13.5.6" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 67e7dec3..3485cae4 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "build:ci": "npm run build && npm run bundle:check", "test:ci": "npm run test -- --run && npm run build:ci", "test:s0": "npx tsc -b --pretty false && npm run test -- --run src/store.test.ts src/hooks/useFlowHistory.test.ts src/components/flow-canvas/flowCanvasTypes.test.ts src/services/mermaid/parseMermaidByType.test.ts src/services/exportService.test.ts src/components/flow-canvas/largeGraphSafetyMode.test.ts", + "test:mermaid:gold": "npm run test:mermaid && npm run test:mermaid:layout", + "test:mermaid": "npx tsc -b --pretty false && npm run test -- --run src/services/mermaid/*.test.ts src/diagram-types/*/*.test.ts src/services/*RoundTrip.test.ts", + "test:mermaid:layout": "npx tsc -b --pretty false && npm run test -- --run src/services/mermaid/mermaidLayoutCorpus.test.ts src/services/mermaid/compatFixtureCorpus.test.ts src/services/mermaid/editablePartialCorpus.test.ts", "test:s10-state": "npx tsc -b --pretty false && npm run test -- --run src/diagram-types/stateDiagram/plugin.test.ts src/services/stateDiagramRoundTrip.test.ts src/services/exportService.test.ts src/services/mermaid/parseMermaidByType.test.ts src/services/mermaid/detectDiagramType.test.ts", "test:s10-canvas": "npx tsc -b --pretty false && npm run test -- --run src/components/flow-canvas/pasteHelpers.test.ts src/components/flow-canvas/flowCanvasTypes.test.ts src/components/flow-canvas/largeGraphSafetyMode.test.ts src/hooks/edgeConnectInteractions.test.ts src/components/DesignSystem.integration.test.tsx src/components/HomePage.integration.test.tsx src/store.test.ts", "test:s4-handle-safety": "npx tsc -b --pretty false && npm run test -- --run src/components/handleInteraction.test.ts src/components/handleInteractionUsage.test.ts src/components/CustomNode.handleInteraction.test.tsx src/components/custom-nodes/ArchitectureNode.handleInteraction.test.tsx src/components/custom-nodes/ClassEntityNode.handleInteraction.test.tsx src/components/lightweight-nodes.handleInteraction.test.tsx src/components/custom-nodes/VisualNodes.handleInteraction.test.tsx src/components/container-nodes.handleInteraction.test.tsx src/components/flow-canvas/flowCanvasTypes.test.ts src/components/DesignSystem.integration.test.tsx src/components/HomePage.integration.test.tsx", @@ -37,6 +40,7 @@ "bench:harness": "node scripts/benchmark-harness.mjs", "bench:summary": "node scripts/benchmark-summary.mjs", "bench:check": "node scripts/check-benchmark-results.mjs", + "mermaid:compat-report": "node scripts/mermaid-compat-report.mjs", "audit:reactflow-v12": "node scripts/audit-reactflow-v12.mjs", "shape-pack:manifest": "node scripts/shape-pack/generate-shape-pack-manifest.mjs", "shape-pack:validate": "node scripts/shape-pack/validate-shape-pack-manifests.mjs", @@ -45,7 +49,9 @@ }, "dependencies": { "@google/genai": "^1.40.0", + "@mermaid-js/layout-elk": "^0.2.1", "@xyflow/react": "^12.10.1", + "dagre": "^0.8.5", "elkjs": "^0.11.0", "framer-motion": "^12.34.0", "html-to-image": "^1.11.13", @@ -54,6 +60,7 @@ "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", "lucide-react": "^0.563.0", + "mermaid": "^11.14.0", "posthog-js": "^1.347.2", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -74,6 +81,7 @@ "@tailwindcss/postcss": "^4.1.18", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@types/dagre": "^0.7.54", "@types/node": "^22.14.0", "@types/react-syntax-highlighter": "^15.5.13", "@typescript-eslint/eslint-plugin": "^8.55.0", diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index d854fedb..ef465ba6 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -856,7 +856,11 @@ "architectureStrictMode": "Architektur-Strict-Modus", "architectureStrictModeDesc": "Mermaid-Import blockieren, wenn Architekturdiagnosen Wiederherstellungs-/Validierungsprobleme enthalten", "miniMap": "Minimap", - "miniMapDesc": "Minimap unten rechts anzeigen" + "miniMapDesc": "Minimap unten rechts anzeigen", + "mermaidImportMode": "Mermaid Import Mode", + "mermaidImportModeRenderer": "Fidelity-first", + "mermaidImportModeEditable": "Editable-first", + "mermaidImportModeDesc": "Controls how Mermaid diagrams are imported to the canvas." }, "ai": { "provider": "Anbieter", @@ -1441,4 +1445,4 @@ "aiModel": { "buttons": {} } -} +} \ No newline at end of file diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index b82a69d9..a1f0e716 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -949,7 +949,11 @@ "routingProfileInfrastructure": "Infrastructure", "routingProfileHint": "Infrastructure mode biases orthogonal routes for service maps.", "edgeBundling": "Bundle Sibling Edges", - "edgeBundlingDesc": "Keep parallel connections on shared lanes" + "edgeBundlingDesc": "Keep parallel connections on shared lanes", + "mermaidImportMode": "Mermaid Import Mode", + "mermaidImportModeRenderer": "Fidelity-first", + "mermaidImportModeEditable": "Editable-first", + "mermaidImportModeDesc": "Controls how Mermaid diagrams are imported to the canvas." }, "analytics": { "enableTitle": "Anonymous Launch Analytics", @@ -1436,4 +1440,4 @@ "sidebar": { "close": "Close sidebar" } -} +} \ No newline at end of file diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 16024368..5ccd61a6 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -856,7 +856,11 @@ "architectureStrictMode": "Modo Estricto de Arquitectura", "architectureStrictModeDesc": "Bloquear importaciรณn de Mermaid cuando los diagnรณsticos de arquitectura incluyan problemas de recuperaciรณn/validaciรณn", "miniMap": "Minimapa", - "miniMapDesc": "Mostrar minimapa en la esquina inferior derecha" + "miniMapDesc": "Mostrar minimapa en la esquina inferior derecha", + "mermaidImportMode": "Mermaid Import Mode", + "mermaidImportModeRenderer": "Fidelity-first", + "mermaidImportModeEditable": "Editable-first", + "mermaidImportModeDesc": "Controls how Mermaid diagrams are imported to the canvas." }, "ai": { "provider": "Proveedor", @@ -1441,4 +1445,4 @@ "aiModel": { "buttons": {} } -} +} \ No newline at end of file diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 3f6bdcf4..824bb17c 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -856,7 +856,11 @@ "architectureStrictMode": "Mode Strict Architecture", "architectureStrictModeDesc": "Bloquer l'import Mermaid lorsque les diagnostics d'architecture incluent des problรจmes de rรฉcupรฉration/validation", "miniMap": "Mini carte", - "miniMapDesc": "Afficher la mini carte en bas ร  droite" + "miniMapDesc": "Afficher la mini carte en bas ร  droite", + "mermaidImportMode": "Mermaid Import Mode", + "mermaidImportModeRenderer": "Fidelity-first", + "mermaidImportModeEditable": "Editable-first", + "mermaidImportModeDesc": "Controls how Mermaid diagrams are imported to the canvas." }, "ai": { "provider": "Fournisseur", @@ -1441,4 +1445,4 @@ "aiModel": { "buttons": {} } -} +} \ No newline at end of file diff --git a/public/locales/ja/translation.json b/public/locales/ja/translation.json index f3b5597e..16690c1f 100644 --- a/public/locales/ja/translation.json +++ b/public/locales/ja/translation.json @@ -856,7 +856,11 @@ "architectureStrictMode": "ใ‚ขใƒผใ‚ญใƒ†ใ‚ฏใƒใƒฃStrictใƒขใƒผใƒ‰", "architectureStrictModeDesc": "ใ‚ขใƒผใ‚ญใƒ†ใ‚ฏใƒใƒฃ่จบๆ–ญใŒๅ›žๅพฉ/ๆคœ่จผใฎๅ•้กŒใ‚’ๅซใ‚€ๅ ดๅˆใ€Mermaidใ‚คใƒณใƒใƒผใƒˆใ‚’ใƒ–ใƒญใƒƒใ‚ฏ", "miniMap": "ใƒŸใƒ‹ใƒžใƒƒใƒ—", - "miniMapDesc": "ๅณไธ‹ใซใƒŸใƒ‹ใƒžใƒƒใƒ—ใ‚’่กจ็คบ" + "miniMapDesc": "ๅณไธ‹ใซใƒŸใƒ‹ใƒžใƒƒใƒ—ใ‚’่กจ็คบ", + "mermaidImportMode": "Mermaid Import Mode", + "mermaidImportModeRenderer": "Fidelity-first", + "mermaidImportModeEditable": "Editable-first", + "mermaidImportModeDesc": "Controls how Mermaid diagrams are imported to the canvas." }, "ai": { "provider": "ใƒ—ใƒญใƒใ‚คใƒ€ใƒผ", @@ -1441,4 +1445,4 @@ "aiModel": { "buttons": {} } -} +} \ No newline at end of file diff --git a/public/locales/tr/translation.json b/public/locales/tr/translation.json index 26ef6916..34c37abb 100644 --- a/public/locales/tr/translation.json +++ b/public/locales/tr/translation.json @@ -876,7 +876,11 @@ "architectureStrictMode": "Mimari Katฤฑ Modu", "architectureStrictModeDesc": "Mimari tanฤฑlamalar kurtarma/doฤŸrulama sorunlarฤฑ iรงerdiฤŸinde Mermaid iรงe aktarmayฤฑ engelle", "miniMap": "Mini Harita", - "miniMapDesc": "SaฤŸ altta mini haritayฤฑ gรถster" + "miniMapDesc": "SaฤŸ altta mini haritayฤฑ gรถster", + "mermaidImportMode": "Mermaid Import Mode", + "mermaidImportModeRenderer": "Fidelity-first", + "mermaidImportModeEditable": "Editable-first", + "mermaidImportModeDesc": "Controls how Mermaid diagrams are imported to the canvas." }, "ai": { "provider": "SaฤŸlayฤฑcฤฑ", @@ -1441,4 +1445,4 @@ "aiModel": { "buttons": {} } -} +} \ No newline at end of file diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index bdfb4b64..2a304210 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -856,7 +856,11 @@ "architectureStrictMode": "ๆžถๆž„ไธฅๆ ผๆจกๅผ", "architectureStrictModeDesc": "ๅฝ“ๆžถๆž„่ฏŠๆ–ญๅŒ…ๅซๆขๅค/้ชŒ่ฏ้—ฎ้ข˜ๆ—ถ้˜ปๆญขMermaidๅฏผๅ…ฅ", "miniMap": "ๅฐๅœฐๅ›พ", - "miniMapDesc": "ๅœจๅณไธ‹่ง’ๆ˜พ็คบๅฐๅœฐๅ›พ" + "miniMapDesc": "ๅœจๅณไธ‹่ง’ๆ˜พ็คบๅฐๅœฐๅ›พ", + "mermaidImportMode": "Mermaid Import Mode", + "mermaidImportModeRenderer": "Fidelity-first", + "mermaidImportModeEditable": "Editable-first", + "mermaidImportModeDesc": "Controls how Mermaid diagrams are imported to the canvas." }, "ai": { "provider": "ๆไพ›ๅ•†", @@ -1441,4 +1445,4 @@ "aiModel": { "buttons": {} } -} +} \ No newline at end of file diff --git a/scripts/check-bundle-budget.mjs b/scripts/check-bundle-budget.mjs index 1bd8a6c5..8959f4dd 100644 --- a/scripts/check-bundle-budget.mjs +++ b/scripts/check-bundle-budget.mjs @@ -37,6 +37,20 @@ function readAllChunkSizes() { return sizes; } +function isStaticAssetWrapperChunk(relativePath) { + const filePath = path.join(DIST_DIR, relativePath); + const source = fs.readFileSync(filePath, 'utf8').trim(); + + // Vite emits tiny JS modules for SVG/data-URL assets when we use import.meta.glob with + // `?url` or when small SVGs are inlined. These modules are static string exports, not + // executable application code, so including them in the lazy-JS total creates false + // bundle-budget failures for large icon catalogs. + return /^const\s+\w+=["'`][\s\S]*["'`];export\{\w+ as default\};?$/.test(source) + && !source.includes('import') + && !source.includes('function') + && !source.includes('=>'); +} + function readEntryAssetSizes() { if (!fs.existsSync(INDEX_HTML_PATH)) { throw new Error('Missing dist/index.html. Run "npm run build" before "npm run bundle:check".'); @@ -108,7 +122,9 @@ function main() { // Lazy chunk checks const allChunkSizes = readAllChunkSizes(); const entryJsSet = new Set(jsEntries.map(([file]) => file)); - const lazyChunks = Array.from(allChunkSizes.entries()).filter(([file]) => !entryJsSet.has(file)); + const lazyChunks = Array.from(allChunkSizes.entries()).filter( + ([file]) => !entryJsSet.has(file) && !isStaticAssetWrapperChunk(file) + ); const totalLazyBytes = lazyChunks.reduce((sum, [, size]) => sum + size, 0); const top5 = [...lazyChunks].sort(([, a], [, b]) => b - a).slice(0, 5); diff --git a/scripts/mermaid-compat-fixtures.json b/scripts/mermaid-compat-fixtures.json new file mode 100644 index 00000000..e33a1cf0 --- /dev/null +++ b/scripts/mermaid-compat-fixtures.json @@ -0,0 +1,338 @@ +[ + { + "name": "flowchart-basic", + "family": "flowchart", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\nA[Start] --> B[End]" + }, + { + "name": "flowchart-subgraph-explicit-id", + "family": "flowchart", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\nsubgraph api[API Layer]\n A[Gateway] --> B[Service]\nend" + }, + { + "name": "flowchart-invalid-edge", + "family": "flowchart", + "expectedOfficial": "invalid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\nA -->" + }, + { + "name": "flowchart-invalid-subgraph-close", + "family": "flowchart", + "expectedOfficial": "invalid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\nsubgraph api[API]\n A --> B" + }, + { + "name": "state-basic", + "family": "stateDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "stateDiagram-v2\n[*] --> Idle\nIdle --> [*]" + }, + { + "name": "state-invalid-direction", + "family": "stateDiagram", + "expectedOfficial": "environment_limited", + "expectedEditableGate": "supported_family", + "source": "stateDiagram-v2\ndirection RLX\n[*] --> Idle" + }, + { + "name": "sequence-basic", + "family": "sequence", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "sequenceDiagram\nparticipant Alice\nparticipant Bob\nAlice->>Bob: Hello" + }, + { + "name": "sequence-activation-and-alt", + "family": "sequence", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "sequenceDiagram\nparticipant A\nparticipant B\nA->>B: Request\nactivate B\nalt success\n B-->>A: Response\nelse failure\n B-->>A: Error\nend\ndeactivate B" + }, + { + "name": "sequence-invalid-message", + "family": "sequence", + "expectedOfficial": "invalid", + "expectedEditableGate": "supported_family", + "source": "sequenceDiagram\nparticipant A\nA->>" + }, + { + "name": "sequence-partial-after-valid-message", + "family": "sequence", + "expectedOfficial": "invalid", + "expectedEditableGate": "supported_family", + "source": "sequenceDiagram\nparticipant A\nparticipant B\nA->>B: Hello\nA->>" + }, + { + "name": "class-basic", + "family": "classDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "classDiagram\nclass Animal\nclass Duck\nAnimal <|-- Duck" + }, + { + "name": "class-cardinality-generic", + "family": "classDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "classDiagram\nclass Repository~T~\nclass User\nRepository~T~ \"1\" --> \"*\" User : stores" + }, + { + "name": "class-generic-multi-param", + "family": "classDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "classDiagram\nclass Map~K, V~\nclass Entry\nMap~K, V~ --> Entry : stores" + }, + { + "name": "class-invalid-relation", + "family": "classDiagram", + "expectedOfficial": "environment_limited", + "expectedEditableGate": "supported_family", + "source": "classDiagram\nclass User\nUser -> Account" + }, + { + "name": "er-basic", + "family": "erDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "erDiagram\nCUSTOMER ||--o{ ORDER : places" + }, + { + "name": "er-field-metadata", + "family": "erDiagram", + "expectedOfficial": "invalid", + "expectedEditableGate": "supported_family", + "source": "erDiagram\nORDER {\n uuid id PK\n uuid customer_id FK REFERENCES CUSTOMER.id\n string external_id UNIQUE\n}" + }, + { + "name": "er-field-references-table-only", + "family": "erDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "erDiagram\nORDER {\n uuid customer_id FK REFERENCES CUSTOMER\n string external_id UK\n}" + }, + { + "name": "er-field-dotted-reference", + "family": "erDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "erDiagram\nORDER {\n uuid customer_id FK REFERENCES billing.Customer.id\n}" + }, + { + "name": "mindmap-basic", + "family": "mindmap", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "mindmap\n Root\n Branch A\n Branch B" + }, + { + "name": "mindmap-wrapped-nodes", + "family": "mindmap", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "mindmap\n root((Root))\n feature[[Child A]]\n decision{{Child B}}" + }, + { + "name": "mindmap-dotted-aliases", + "family": "mindmap", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "mindmap\n platform.root((Root))\n platform.api[[Child A]]\n platform.branch(Child B)" + }, + { + "name": "journey-basic", + "family": "journey", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "journey\ntitle Checkout\nsection Happy\n Search: 5: User\n Buy: 3: User" + }, + { + "name": "journey-multiple-sections", + "family": "journey", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "journey\ntitle Incident Response\nsection Detect\n Alert fires: 5: SRE\nsection Resolve\n Mitigate: 3: SRE, Platform" + }, + { + "name": "journey-invalid-score", + "family": "journey", + "expectedOfficial": "environment_limited", + "expectedEditableGate": "supported_family", + "source": "journey\ntitle Checkout\nsection Happy\n Search: User" + }, + { + "name": "journey-colon-rich-step", + "family": "journey", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "journey\ntitle Incident Response\nsection Alerts\n HTTP: 500 Error: 1: SRE: On-call\n Recover service: 4: API: Team" + }, + { + "name": "journey-support-escalation", + "family": "journey", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "journey\ntitle Support Escalation\nsection Intake\n Customer reports issue: 2: Support\n Triage severity: 3: Support\nsection Escalation\n Hand off to engineering: 4: Support, Engineering\n Confirm resolution: 5: Support" + }, + { + "name": "state-notes-and-control", + "family": "stateDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "stateDiagram-v2\nstate FanOut <>\nstate FanIn <>\n[*] --> FanOut\nFanOut --> Idle\nIdle --> FanIn\nnote right of Idle: Waiting\nFanIn --> [*]" + }, + { + "name": "architecture-official-basic", + "family": "architecture", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "architecture-beta\nservice api(server)[API]\nservice db(database)[Database]\napi:R --> L:db" + }, + { + "name": "architecture-title-basic", + "family": "architecture", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "architecture-beta\ntitle \"Platform\"\nservice api(server)[API]\nservice db(database)[Database]\napi:R --> L:db" + }, + { + "name": "architecture-extension-labeled-edge", + "family": "architecture", + "expectedOfficial": "invalid", + "expectedEditableGate": "supported_family", + "source": "architecture-beta\nservice api(server)[API]\nservice db(database)[Database]\napi:R --> L:db : SQL" + }, + { + "name": "flowchart-inline-class", + "family": "flowchart", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\nA[API]:::hot --> B[(DB)]\nclassDef hot fill:#f66,stroke:#333" + }, + { + "name": "flowchart-class-assignment-line", + "family": "flowchart", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\nA[API]\nB[(DB)]\nclassDef hot fill:#f66,stroke:#333\nclass A,B hot" + }, + { + "name": "flowchart-modern-annotation-dotted-ids", + "family": "flowchart", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\napi.gateway@{ shape: rect, label: \"API Gateway\" } --> db.primary@{ shape: cyl, label: \"Primary DB\" }" + }, + { + "name": "flowchart-style-dotted-id", + "family": "flowchart", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\napi.gateway[Gateway]\nstyle api.gateway fill:#dff,stroke:#08c,color:#024" + }, + { + "name": "flowchart-nested-subgraphs", + "family": "flowchart", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\nsubgraph platform[Platform]\n subgraph api[API]\n gateway[Gateway] --> service[Service]\n end\nend" + }, + { + "name": "flowchart-unexpected-end", + "family": "flowchart", + "expectedOfficial": "invalid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\nA --> B\nend" + }, + { + "name": "flowchart-auth-decision", + "family": "flowchart", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "flowchart TD\nuser[User] --> gateway[API Gateway]\ngateway --> auth{Authenticated?}\nauth -->|Yes| app[Dashboard]\nauth -->|No| login[Login]" + }, + { + "name": "state-composite-alias", + "family": "stateDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "stateDiagram-v2\nstate \"Working Set\" as WorkingSet {\n [*] --> Busy\n Busy --> Idle\n}\nnote left of WorkingSet: Parent note\nIdle --> [*]" + }, + { + "name": "state-direction-lr", + "family": "stateDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "stateDiagram-v2\ndirection LR\n[*] --> Idle\nIdle --> Running\nRunning --> [*]" + }, + { + "name": "state-composite-standalone-declarations", + "family": "stateDiagram", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "stateDiagram-v2\nstate Working {\n state Busy\n state Idle\n Busy --> Idle\n}\nIdle --> [*]" + }, + { + "name": "sequence-par-and", + "family": "sequence", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "sequenceDiagram\nparticipant A\nparticipant B\nparticipant C\npar fast lane\n A->>B: Request\nand slow lane\n A->>C: Request\nend" + }, + { + "name": "sequence-note-inside-alt", + "family": "sequence", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "sequenceDiagram\nparticipant A\nparticipant B\nalt success\n note over A, B: Shared context\n A->>B: Request\nelse failure\n B-->>A: Error\nend" + }, + { + "name": "sequence-critical-option", + "family": "sequence", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "sequenceDiagram\nparticipant A\nparticipant B\ncritical primary path\n A->>B: Request\noption fallback path\n B-->>A: Error\nend" + }, + { + "name": "architecture-rich-node-kinds", + "family": "architecture", + "expectedOfficial": "invalid", + "expectedEditableGate": "supported_family", + "source": "architecture-beta\nperson user[User]\ncontainer app(server)[App]\ndatabase_container data(database)[Data Store]\nuser:R --> L:app : HTTPS\napp:R --> L:data : TCP:5432" + }, + { + "name": "architecture-nested-groups", + "family": "architecture", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "architecture-beta\ngroup global[Global]\ngroup prod(cloud)[Prod] in global\nservice api(server)[API] in prod" + }, + { + "name": "architecture-edge-tier", + "family": "architecture", + "expectedOfficial": "valid", + "expectedEditableGate": "supported_family", + "source": "architecture-beta\ngroup edge[Edge]\nservice web(server)[Web] in edge\nservice api(server)[API]\nservice db(database)[Database]\nweb:R --> L:api : HTTPS:443\napi:R --> L:db : TCP:5432" + }, + { + "name": "unsupported-gitgraph", + "family": "gitGraph", + "expectedOfficial": "valid", + "expectedEditableGate": "unsupported_family", + "source": "gitGraph\ncommit id: \"A\"" + }, + { + "name": "missing-header-flow-only", + "family": "flowchart", + "expectedOfficial": "invalid", + "expectedEditableGate": "invalid_source", + "source": "A --> B" + } +] diff --git a/scripts/mermaid-compat-fixtures.mjs b/scripts/mermaid-compat-fixtures.mjs new file mode 100644 index 00000000..814f022a --- /dev/null +++ b/scripts/mermaid-compat-fixtures.mjs @@ -0,0 +1,342 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const fixturesPath = path.join(__dirname, 'mermaid-compat-fixtures.json'); + +const rawFixtures = JSON.parse(fs.readFileSync(fixturesPath, 'utf8')); + +const FIXTURE_METADATA = { + 'flowchart-basic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, requiredLabels: ['Start', 'End'] }, + layoutAssertions: { + maxBoundingWidth: 260, + maxBoundingHeight: 240, + requireUniquePositions: true, + orderedLabelsTopToBottom: ['Start', 'End'], + }, + }, + 'flowchart-subgraph-explicit-id': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 3, minEdges: 1, minSections: 1, requiredLabels: ['API Layer'] }, + layoutAssertions: { maxBoundingWidth: 420, maxBoundingHeight: 320, minSections: 1 }, + }, + 'flowchart-invalid-edge': { + bucket: 'valid_but_not_editable', + expectedImportState: 'unsupported_construct', + structuralAssertions: { maxNodes: 0, maxEdges: 0 }, + }, + 'flowchart-invalid-subgraph-close': { + bucket: 'editable_partial', + expectedImportState: 'editable_partial', + structuralAssertions: { minNodes: 2, minEdges: 1, diagnosticsMin: 1, minSections: 1 }, + }, + 'state-basic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 3, minEdges: 2 }, + layoutAssertions: { maxBoundingWidth: 420, maxBoundingHeight: 320, requireUniquePositions: true }, + }, + 'state-invalid-direction': { + bucket: 'editable_partial', + expectedImportState: 'editable_partial', + structuralAssertions: { minNodes: 2, minEdges: 1, diagnosticsMin: 1 }, + }, + 'sequence-basic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, minParticipants: 2 }, + layoutAssertions: { + minParticipants: 2, + requireSequenceLaneAlignment: true, + maxBoundingWidth: 520, + orderedLabelsLeftToRight: ['Alice', 'Bob'], + sameRowLabels: ['Alice', 'Bob'], + }, + }, + 'sequence-activation-and-alt': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 3, minEdges: 2, minParticipants: 2, minAnnotations: 1 }, + layoutAssertions: { minParticipants: 2, requireSequenceLaneAlignment: true, maxBoundingWidth: 620 }, + }, + 'sequence-invalid-message': { + bucket: 'editable_partial', + expectedImportState: 'editable_partial', + structuralAssertions: { minNodes: 1, maxEdges: 0, diagnosticsMin: 1, minParticipants: 1 }, + }, + 'sequence-partial-after-valid-message': { + bucket: 'editable_partial', + expectedImportState: 'editable_partial', + structuralAssertions: { minNodes: 2, minEdges: 1, diagnosticsMin: 1, minParticipants: 2 }, + }, + 'class-basic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1 }, + }, + 'class-cardinality-generic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, requiredNodeIds: ['Repository', 'User'] }, + }, + 'class-generic-multi-param': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, requiredNodeIds: ['Map', 'Entry'] }, + }, + 'class-invalid-relation': { + bucket: 'editable_partial', + expectedImportState: 'editable_partial', + structuralAssertions: { minNodes: 1, diagnosticsMin: 1 }, + }, + 'er-basic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1 }, + }, + 'er-field-metadata': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 1, maxEdges: 0, requiredLabels: ['ORDER'] }, + }, + 'er-field-references-table-only': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 1, maxEdges: 0, requiredLabels: ['ORDER'] }, + }, + 'er-field-dotted-reference': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 1, maxEdges: 0, requiredLabels: ['ORDER'] }, + }, + 'mindmap-basic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 3, minEdges: 2 }, + }, + 'mindmap-wrapped-nodes': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 3, minEdges: 2, requiredLabels: ['Root', 'Child A', 'Child B'] }, + }, + 'mindmap-dotted-aliases': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { + minNodes: 3, + minEdges: 2, + requiredLabels: ['Root', 'Child A', 'Child B'], + }, + }, + 'journey-basic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, requiredLabels: ['Search', 'Buy'] }, + layoutAssertions: { + maxBoundingHeight: 220, + orderedLabelsTopToBottom: ['Search', 'Buy'], + }, + }, + 'journey-multiple-sections': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, maxEdges: 0, requiredLabels: ['Alert fires', 'Mitigate'] }, + }, + 'journey-invalid-score': { + bucket: 'valid_but_not_editable', + expectedImportState: 'unsupported_construct', + structuralAssertions: { maxNodes: 0, maxEdges: 0, diagnosticsMin: 1 }, + }, + 'journey-colon-rich-step': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, requiredLabels: ['HTTP: 500 Error', 'Recover service'] }, + }, + 'journey-support-escalation': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { + minNodes: 4, + minEdges: 2, + requiredLabels: ['Customer reports issue', 'Triage severity', 'Hand off to engineering', 'Confirm resolution'], + }, + }, + 'state-notes-and-control': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 4, minEdges: 4, requiredLabels: ['Idle'] }, + }, + 'architecture-official-basic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, requiredLabels: ['API', 'Database'] }, + layoutAssertions: { + maxBoundingWidth: 420, + maxBoundingHeight: 260, + requireUniquePositions: true, + orderedLabelsLeftToRight: ['API', 'Database'], + }, + }, + 'architecture-title-basic': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, requiredLabels: ['API', 'Database'] }, + }, + 'architecture-extension-labeled-edge': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1 }, + }, + 'flowchart-inline-class': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, requiredLabels: ['API', 'DB'] }, + }, + 'flowchart-class-assignment-line': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, maxEdges: 0, requiredLabels: ['API', 'DB'] }, + }, + 'flowchart-modern-annotation-dotted-ids': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1, requiredNodeIds: ['api.gateway', 'db.primary'] }, + layoutAssertions: { maxBoundingWidth: 320, maxBoundingHeight: 240, requireUniquePositions: true }, + }, + 'flowchart-style-dotted-id': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 1, maxEdges: 0, requiredNodeIds: ['api.gateway'], requiredLabels: ['Gateway'] }, + layoutAssertions: { maxBoundingWidth: 200, maxBoundingHeight: 120 }, + }, + 'flowchart-nested-subgraphs': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { + minNodes: 4, + minEdges: 1, + minSections: 2, + requiredLabels: ['Platform', 'API', 'Gateway', 'Service'], + requiredParentIds: { + api: 'platform', + gateway: 'api', + service: 'api', + }, + }, + layoutAssertions: { maxBoundingWidth: 460, maxBoundingHeight: 360, minSections: 2 }, + }, + 'flowchart-unexpected-end': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 2, minEdges: 1 }, + }, + 'flowchart-auth-decision': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { + minNodes: 5, + minEdges: 4, + requiredLabels: ['User', 'API Gateway', 'Authenticated?', 'Dashboard', 'Login'], + }, + }, + 'state-composite-alias': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 4, minEdges: 3, minSections: 1, requiredLabels: ['Working Set'] }, + }, + 'state-direction-lr': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 4, minEdges: 3 }, + layoutAssertions: { maxBoundingWidth: 520, maxBoundingHeight: 440, requireUniquePositions: true }, + }, + 'state-composite-standalone-declarations': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { + minNodes: 3, + minEdges: 2, + requiredLabels: ['Working', 'Busy', 'Idle'], + requiredParentIds: { + Busy: 'Working', + Idle: 'Working', + }, + }, + layoutAssertions: { maxBoundingWidth: 420, maxBoundingHeight: 320, requireUniquePositions: true }, + }, + 'sequence-par-and': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 3, minEdges: 2, minParticipants: 3 }, + layoutAssertions: { minParticipants: 3, requireSequenceLaneAlignment: true, maxBoundingWidth: 760 }, + }, + 'sequence-note-inside-alt': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 3, minEdges: 2, minParticipants: 2, minNotes: 1, minAnnotations: 1 }, + layoutAssertions: { minParticipants: 2, requireSequenceLaneAlignment: true, requireNotesBelowParticipants: true, maxBoundingWidth: 620 }, + }, + 'sequence-critical-option': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 4, minEdges: 2, minParticipants: 2, minAnnotations: 2 }, + layoutAssertions: { minParticipants: 2, requireSequenceLaneAlignment: true, maxBoundingWidth: 620 }, + }, + 'architecture-rich-node-kinds': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { minNodes: 3, minEdges: 2, requiredLabels: ['User', 'App', 'Data Store'] }, + }, + 'architecture-nested-groups': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { + minNodes: 3, + maxEdges: 0, + requiredLabels: ['Global', 'Prod', 'API'], + requiredParentIds: { + prod: 'global', + api: 'prod', + }, + }, + }, + 'architecture-edge-tier': { + bucket: 'editable_full', + expectedImportState: 'editable_full', + structuralAssertions: { + minNodes: 4, + minEdges: 2, + requiredLabels: ['Edge', 'Web', 'API', 'Database'], + }, + }, + 'unsupported-gitgraph': { + bucket: 'valid_but_not_editable', + expectedImportState: 'unsupported_family', + structuralAssertions: { maxNodes: 0, maxEdges: 0 }, + }, + 'missing-header-flow-only': { + bucket: 'invalid_source', + expectedImportState: 'invalid_source', + structuralAssertions: { maxNodes: 0, maxEdges: 0, diagnosticsMin: 1 }, + }, +}; + +function enrichFixture(fixture) { + const metadata = FIXTURE_METADATA[fixture.name]; + if (!metadata) { + throw new Error(`Missing metadata for Mermaid compat fixture "${fixture.name}".`); + } + + return { + ...fixture, + ...metadata, + }; +} + +export const MERMAID_COMPAT_FIXTURES = rawFixtures.map(enrichFixture); diff --git a/scripts/mermaid-compat-report.mjs b/scripts/mermaid-compat-report.mjs new file mode 100644 index 00000000..800635a0 --- /dev/null +++ b/scripts/mermaid-compat-report.mjs @@ -0,0 +1,135 @@ +import mermaid from 'mermaid'; +import { MERMAID_COMPAT_FIXTURES as fixtures } from './mermaid-compat-fixtures.mjs'; + +mermaid.initialize({ + startOnLoad: false, + securityLevel: 'loose', + suppressErrorRendering: true, +}); + +const SUPPORTED_EDITABLE_FAMILIES = new Set([ + 'flowchart', + 'flowchart-v2', + 'stateDiagram', + 'stateDiagram-v2', + 'class', + 'classDiagram', + 'er', + 'erDiagram', + 'mindmap', + 'journey', + 'architecture', + 'architecture-beta', + 'sequence', + 'sequenceDiagram', +]); + +function classifyOfficialResult(official) { + if (official.isValid) return 'valid'; + if (official.diagnostics.some((message) => message.includes('DOMPurify'))) { + return 'environment_limited'; + } + return 'invalid'; +} + +const results = []; + +for (const fixture of fixtures) { + let official = { + isValid: false, + rawType: null, + diagnostics: [], + validationMode: 'full', + }; + + try { + const rawType = mermaid.detectType(fixture.source); + official.rawType = rawType; + const parsed = await mermaid.parse(fixture.source, { suppressErrors: false }); + official.isValid = Boolean(parsed); + } catch (error) { + official.diagnostics.push(error instanceof Error ? error.message : String(error)); + } + + const editableSupport = + official.rawType && SUPPORTED_EDITABLE_FAMILIES.has(official.rawType) + ? 'supported_family' + : official.rawType + ? 'unsupported_family' + : 'invalid_source'; + const officialStatus = classifyOfficialResult(official); + + results.push({ + name: fixture.name, + family: fixture.family, + expectedOfficial: fixture.expectedOfficial, + expectedEditableGate: fixture.expectedEditableGate, + official: { + isValid: official.isValid, + rawType: official.rawType ?? null, + status: officialStatus, + validationMode: official.validationMode, + diagnostics: official.diagnostics, + }, + editableGate: { + status: editableSupport, + }, + matchesExpectation: { + official: + fixture.expectedOfficial === 'valid' + ? officialStatus === 'valid' || officialStatus === 'environment_limited' + : officialStatus === fixture.expectedOfficial, + editableGate: editableSupport === fixture.expectedEditableGate, + }, + }); +} + +const familySummary = Object.values( + results.reduce((acc, entry) => { + const existing = acc[entry.family] ?? { + family: entry.family, + total: 0, + officialValid: 0, + officialEnvironmentLimited: 0, + officialInvalid: 0, + supportedFamilies: 0, + unsupportedFamilies: 0, + invalidSources: 0, + officialExpectationMatches: 0, + editableExpectationMatches: 0, + }; + + existing.total += 1; + if (entry.official.status === 'valid') existing.officialValid += 1; + if (entry.official.status === 'environment_limited') existing.officialEnvironmentLimited += 1; + if (entry.official.status === 'invalid') existing.officialInvalid += 1; + if (entry.editableGate.status === 'supported_family') existing.supportedFamilies += 1; + if (entry.editableGate.status === 'unsupported_family') existing.unsupportedFamilies += 1; + if (entry.editableGate.status === 'invalid_source') existing.invalidSources += 1; + if (entry.matchesExpectation.official) existing.officialExpectationMatches += 1; + if (entry.matchesExpectation.editableGate) existing.editableExpectationMatches += 1; + + acc[entry.family] = existing; + return acc; + }, {}) +).sort((left, right) => left.family.localeCompare(right.family)); + +const summary = { + generatedAt: new Date().toISOString(), + totalFixtures: results.length, + officialValid: results.filter((entry) => entry.official.status === 'valid').length, + officialEnvironmentLimited: results.filter( + (entry) => entry.official.status === 'environment_limited' + ).length, + officialInvalid: results.filter((entry) => entry.official.status === 'invalid').length, + supportedFamilies: results.filter((entry) => entry.editableGate.status === 'supported_family') + .length, + unsupportedFamilies: results.filter((entry) => entry.editableGate.status === 'unsupported_family') + .length, + invalid: results.filter((entry) => entry.editableGate.status === 'invalid_source').length, + officialExpectationMatches: results.filter((entry) => entry.matchesExpectation.official).length, + editableExpectationMatches: results.filter((entry) => entry.matchesExpectation.editableGate) + .length, +}; + +console.log(JSON.stringify({ summary, familySummary, results }, null, 2)); diff --git a/scripts/mermaid-flowchart-gold-corpus.json b/scripts/mermaid-flowchart-gold-corpus.json new file mode 100644 index 00000000..dd548c7d --- /dev/null +++ b/scripts/mermaid-flowchart-gold-corpus.json @@ -0,0 +1,62 @@ +[ + { + "id": "gold-flowchart-auth-decision", + "fixtureName": "flowchart-auth-decision", + "priority": "p0", + "failureModes": ["edge_route_drift", "rank_order_drift"], + "userImpact": "Decision branches become hard to scan when route ownership or branching direction drifts from Mermaid.", + "successCriteria": [ + "Decision node remains visually above both outcomes.", + "Yes and No branches stay clearly separated left-to-right.", + "Imported result stays in a renderer-backed Mermaid outcome when possible." + ] + }, + { + "id": "gold-flowchart-nested-subgraphs", + "fixtureName": "flowchart-nested-subgraphs", + "priority": "p0", + "failureModes": ["container_geometry_drift", "parent_child_containment_drift"], + "userImpact": "Nested subgraphs look unreliable when parent boxes, child offsets, or containment structure drift.", + "successCriteria": [ + "Outer and inner containers keep stable nesting relationships.", + "Child nodes stay inside their intended subgraph.", + "Container geometry remains visibly intentional rather than auto-fit noise." + ] + }, + { + "id": "gold-flowchart-subgraph-explicit-id", + "fixtureName": "flowchart-subgraph-explicit-id", + "priority": "p1", + "failureModes": ["container_geometry_drift", "identifier_reconciliation_drift"], + "userImpact": "Explicit Mermaid section ids must map cleanly into editable sections or users lose trust in imported structure.", + "successCriteria": [ + "Explicit subgraph id stays stable through import.", + "Section title and bounds remain readable.", + "Nodes remain attached to the correct imported section." + ] + }, + { + "id": "gold-flowchart-modern-dotted-ids", + "fixtureName": "flowchart-modern-annotation-dotted-ids", + "priority": "p1", + "failureModes": ["identifier_reconciliation_drift", "node_sizing_readability_drift"], + "userImpact": "Modern Mermaid ids with dots are common in infrastructure diagrams and break trust fast when labels or identity drift.", + "successCriteria": [ + "Dotted ids survive reconciliation intact.", + "Imported node labels remain readable without overlap.", + "Result does not silently degrade identity semantics." + ] + }, + { + "id": "gold-flowchart-basic", + "fixtureName": "flowchart-basic", + "priority": "p2", + "failureModes": ["node_sizing_readability_drift", "rank_order_drift"], + "userImpact": "Even the simplest flowchart must remain visually obvious or all more complex imports feel risky.", + "successCriteria": [ + "Top-to-bottom order remains obvious at a glance.", + "Node spacing stays compact and readable.", + "Baseline case remains exact Mermaid layout." + ] + } +] diff --git a/scripts/mermaid-flowchart-gold-corpus.mjs b/scripts/mermaid-flowchart-gold-corpus.mjs new file mode 100644 index 00000000..6da8c3f8 --- /dev/null +++ b/scripts/mermaid-flowchart-gold-corpus.mjs @@ -0,0 +1,9 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const corpusPath = path.join(__dirname, 'mermaid-flowchart-gold-corpus.json'); + +export const MERMAID_FLOWCHART_GOLD_CORPUS = JSON.parse(fs.readFileSync(corpusPath, 'utf8')); diff --git a/scripts/shape-pack/add-missing-devicons.mjs b/scripts/shape-pack/add-missing-devicons.mjs new file mode 100644 index 00000000..bd6c101a --- /dev/null +++ b/scripts/shape-pack/add-missing-devicons.mjs @@ -0,0 +1,221 @@ +/** + * Fetches missing infrastructure/dev tool icons from Simple Icons CDN + * and appends them to the developer-icons-v1 manifest. + * + * Simple Icons: https://simpleicons.org โ€” CC0 license for most icons. + * Run: node scripts/shape-pack/add-missing-devicons.mjs + */ + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const MANIFEST_PATH = path.resolve(__dirname, '../../assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json'); + +// Missing icons: { id suffix, display label, category, simpleicons slug } +const MISSING_ICONS = [ + // Network / Proxy / Service Mesh + { id: 'infra-nginx', label: 'Nginx', category: 'Infra', slug: 'nginx' }, + { id: 'infra-traefik', label: 'Traefik', category: 'Infra', slug: 'traefikproxy' }, + { id: 'infra-haproxy', label: 'HAProxy', category: 'Infra', slug: 'haproxy' }, + { id: 'infra-envoy', label: 'Envoy', category: 'Infra', slug: 'envoyproxy' }, + { id: 'infra-istio', label: 'Istio', category: 'Infra', slug: 'istio' }, + { id: 'infra-consul', label: 'Consul', category: 'Infra', slug: 'consul' }, + { id: 'infra-kong', label: 'Kong', category: 'Infra', slug: 'kong' }, + { id: 'infra-etcd', label: 'etcd', category: 'Infra', slug: 'etcd' }, + { id: 'infra-linkerd', label: 'Linkerd', category: 'Infra', slug: 'linkerd' }, + { id: 'infra-zookeeper', label: 'Zookeeper', category: 'Infra', slug: 'apachezookeeper' }, + { id: 'infra-caddy', label: 'Caddy', category: 'Infra', slug: 'caddy' }, + { id: 'infra-gunicorn', label: 'Gunicorn', category: 'Infra', slug: 'gunicorn' }, + { id: 'infra-tomcat', label: 'Tomcat', category: 'Infra', slug: 'apachetomcat' }, + + // Monitoring / Observability + { id: 'monitoring-prometheus', label: 'Prometheus', category: 'Monitoring', slug: 'prometheus' }, + { id: 'monitoring-sentry', label: 'Sentry', category: 'Monitoring', slug: 'sentry' }, + { id: 'monitoring-newrelic', label: 'New Relic', category: 'Monitoring', slug: 'newrelic' }, + { id: 'monitoring-datadog', label: 'Datadog', category: 'Monitoring', slug: 'datadog' }, + { id: 'monitoring-dynatrace', label: 'Dynatrace', category: 'Monitoring', slug: 'dynatrace' }, + { id: 'monitoring-splunk', label: 'Splunk', category: 'Monitoring', slug: 'splunk' }, + { id: 'monitoring-nagios', label: 'Nagios', category: 'Monitoring', slug: 'nagios' }, + { id: 'monitoring-zabbix', label: 'Zabbix', category: 'Monitoring', slug: 'zabbix' }, + { id: 'monitoring-jaeger', label: 'Jaeger', category: 'Monitoring', slug: 'jaegertracing' }, + + // Databases + { id: 'database-influxdb', label: 'InfluxDB', category: 'Database', slug: 'influxdb' }, + { id: 'database-cockroachdb', label: 'CockroachDB', category: 'Database', slug: 'cockroachlabs' }, + { id: 'database-neo4j', label: 'Neo4j', category: 'Database', slug: 'neo4j' }, + { id: 'database-couchdb', label: 'CouchDB', category: 'Database', slug: 'apachecouchdb' }, + { id: 'database-couchbase', label: 'Couchbase', category: 'Database', slug: 'couchbase' }, + { id: 'database-duckdb', label: 'DuckDB', category: 'Database', slug: 'duckdb' }, + { id: 'database-qdrant', label: 'Qdrant', category: 'Database', slug: 'qdrant' }, + { id: 'database-scylla', label: 'ScyllaDB', category: 'Database', slug: 'scylladb' }, + { id: 'database-druid', label: 'Apache Druid', category: 'Database', slug: 'apachedruid' }, + + // Queues / Messaging + { id: 'queue-rabbitmq', label: 'RabbitMQ', category: 'Queue', slug: 'rabbitmq' }, + { id: 'queue-nats', label: 'NATS', category: 'Queue', slug: 'nats-io' }, + { id: 'queue-activemq', label: 'ActiveMQ', category: 'Queue', slug: 'apacheactivemq' }, + { id: 'queue-celery', label: 'Celery', category: 'Queue', slug: 'celery' }, + { id: 'queue-emqx', label: 'EMQX', category: 'Queue', slug: 'emqx' }, + + // Containers / Runtime + { id: 'container-containerd', label: 'containerd', category: 'Container', slug: 'containerd' }, + { id: 'container-k3s', label: 'K3s', category: 'Container', slug: 'k3s' }, + { id: 'container-lxc', label: 'LXC', category: 'Container', slug: 'linuxcontainers' }, + + // Logging + { id: 'logging-loki', label: 'Loki', category: 'Logging', slug: 'grafanaloki' }, + { id: 'logging-fluentbit', label: 'Fluent Bit', category: 'Logging', slug: 'fluentbit' }, + { id: 'logging-fluentd', label: 'Fluentd', category: 'Logging', slug: 'fluentd' }, + { id: 'logging-graylog', label: 'Graylog', category: 'Logging', slug: 'graylog' }, + + // CI + { id: 'ci-travisci', label: 'Travis CI', category: 'CI-CD', slug: 'travisci' }, + { id: 'ci-teamcity', label: 'TeamCity', category: 'CI-CD', slug: 'teamcity' }, + { id: 'ci-droneci', label: 'Drone CI', category: 'CI-CD', slug: 'drone' }, + + // GitOps + { id: 'gitops-argocd', label: 'Argo CD', category: 'GitOps', slug: 'argo' }, + { id: 'gitops-flux', label: 'Flux', category: 'GitOps', slug: 'flux' }, + + // IaC / Infra Management + { id: 'iac-puppet', label: 'Puppet', category: 'IaC', slug: 'puppet' }, + { id: 'iac-ansible', label: 'Ansible', category: 'IaC', slug: 'ansible' }, + { id: 'iac-chef', label: 'Chef', category: 'IaC', slug: 'chef' }, + { id: 'iac-nomad', label: 'Nomad', category: 'IaC', slug: 'hashicorpnomad' }, + + // Storage + { id: 'storage-ceph', label: 'Ceph', category: 'Storage', slug: 'ceph' }, + { id: 'storage-portworx', label: 'Portworx', category: 'Storage', slug: 'portworx' }, + { id: 'storage-glusterfs', label: 'GlusterFS', category: 'Storage', slug: 'gluster' }, + { id: 'storage-minio', label: 'MinIO', category: 'Storage', slug: 'minio' }, + + // Cache / In-Memory + { id: 'cache-memcached', label: 'Memcached', category: 'Cache', slug: 'memcached' }, + { id: 'cache-hazelcast', label: 'Hazelcast', category: 'Cache', slug: 'hazelcast' }, + + // Auth / Identity + { id: 'auth-vault', label: 'Vault', category: 'Security', slug: 'vault' }, + { id: 'auth-keycloak', label: 'Keycloak', category: 'Security', slug: 'keycloak' }, + { id: 'auth-oauth2', label: 'OAuth 2.0', category: 'Security', slug: 'auth0' }, + + // Certs + { id: 'infra-letsencrypt', label: "Let's Encrypt", category: 'Infra', slug: 'letsencrypt' }, + + // Workflow / Orchestration + { id: 'workflow-airflow', label: 'Apache Airflow', category: 'Workflow', slug: 'apacheairflow' }, + { id: 'workflow-kubeflow', label: 'Kubeflow', category: 'Workflow', slug: 'kubeflow' }, + { id: 'workflow-prefect', label: 'Prefect', category: 'Workflow', slug: 'prefect' }, + { id: 'workflow-temporal', label: 'Temporal', category: 'Workflow', slug: 'temporal' }, + + // Registry + { id: 'registry-harbor', label: 'Harbor', category: 'Registry', slug: 'harbor' }, + { id: 'registry-jfrog', label: 'JFrog', category: 'Registry', slug: 'jfrog' }, + + // Analytics / Data + { id: 'analytics-spark', label: 'Apache Spark', category: 'Analytics', slug: 'apachespark' }, + { id: 'analytics-flink', label: 'Apache Flink', category: 'Analytics', slug: 'apacheflink' }, + { id: 'analytics-hadoop', label: 'Hadoop', category: 'Analytics', slug: 'apachehadoop' }, + { id: 'analytics-hive', label: 'Apache Hive', category: 'Analytics', slug: 'apachehive' }, + { id: 'analytics-databricks', label: 'Databricks', category: 'Analytics', slug: 'databricks' }, + { id: 'analytics-dbt', label: 'dbt', category: 'Analytics', slug: 'dbt' }, + { id: 'analytics-superset', label: 'Superset', category: 'Analytics', slug: 'apachesuperset' }, + { id: 'analytics-tableau', label: 'Tableau', category: 'Analytics', slug: 'tableau' }, + { id: 'analytics-powerbi', label: 'Power BI', category: 'Analytics', slug: 'powerbi' }, + { id: 'analytics-trino', label: 'Trino', category: 'Analytics', slug: 'trino' }, + { id: 'analytics-solr', label: 'Apache Solr', category: 'Analytics', slug: 'apachesolr' }, + + // MLOps + { id: 'ml-mlflow', label: 'MLflow', category: 'ML', slug: 'mlflow' }, + { id: 'ml-wandb', label: 'Weights & Biases', category: 'ML', slug: 'weightsandbiases' }, + { id: 'ml-bentoml', label: 'BentoML', category: 'ML', slug: 'bentoml' }, + + // Frameworks + { id: 'backend-dotnet', label: '.NET', category: 'Backend', slug: 'dotnet' }, + { id: 'backend-quarkus', label: 'Quarkus', category: 'Backend', slug: 'quarkus' }, + { id: 'backend-micronaut', label: 'Micronaut', category: 'Backend', slug: 'micronaut' }, + { id: 'backend-phoenix', label: 'Phoenix', category: 'Backend', slug: 'phoenixframework' }, + { id: 'backend-camel', label: 'Apache Camel', category: 'Backend', slug: 'apachecamel' }, + { id: 'backend-dapr', label: 'Dapr', category: 'Backend', slug: 'dapr' }, + + // Languages + { id: 'lang-cpp', label: 'C++', category: 'Languages', slug: 'cplusplus' }, + { id: 'lang-latex', label: 'LaTeX', category: 'Languages', slug: 'latex' }, + { id: 'lang-matlab', label: 'MATLAB', category: 'Languages', slug: 'matlab' }, + { id: 'lang-zig', label: 'Zig', category: 'Languages', slug: 'zig' }, + { id: 'lang-lua', label: 'Lua', category: 'Languages', slug: 'lua' }, + { id: 'lang-dart', label: 'Dart', category: 'Languages', slug: 'dart' }, +]; + +async function fetchSvg(slug) { + const url = `https://cdn.simpleicons.org/${slug}`; + try { + const res = await fetch(url); + if (!res.ok) return null; + const svg = await res.text(); + // Simple Icons SVGs are black by default โ€” good for our use case + return svg.trim(); + } catch { + return null; + } +} + +async function main() { + console.log(`Reading manifest from ${MANIFEST_PATH}`); + const raw = await fs.readFile(MANIFEST_PATH, 'utf8'); + const manifest = JSON.parse(raw); + + const existingIds = new Set(manifest.shapes.map(s => s.id)); + console.log(`Existing shapes: ${manifest.shapes.length}`); + + let added = 0; + let failed = []; + + for (const icon of MISSING_ICONS) { + if (existingIds.has(icon.id)) { + console.log(` SKIP (exists): ${icon.id}`); + continue; + } + + process.stdout.write(` Fetching ${icon.label} (${icon.slug})... `); + const svgContent = await fetchSvg(icon.slug); + + if (!svgContent) { + console.log(`FAILED`); + failed.push(icon); + continue; + } + + manifest.shapes.push({ + id: icon.id, + label: icon.label, + category: icon.category, + svgContent, + defaultWidth: 160, + defaultHeight: 96, + nodeType: 'custom', + defaultData: {}, + }); + + existingIds.add(icon.id); + added++; + console.log(`OK`); + + // Small delay to be polite to CDN + await new Promise(r => setTimeout(r, 50)); + } + + await fs.writeFile(MANIFEST_PATH, JSON.stringify(manifest, null, 2)); + console.log(`\nDone. Added ${added} icons. Total: ${manifest.shapes.length}`); + + if (failed.length > 0) { + console.log(`\nFailed to fetch (${failed.length}):`); + failed.forEach(f => console.log(` - ${f.label} (slug: ${f.slug})`)); + } +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/src/components/ConnectMenu.test.tsx b/src/components/ConnectMenu.test.tsx index 834ef9df..d4f35f7b 100644 --- a/src/components/ConnectMenu.test.tsx +++ b/src/components/ConnectMenu.test.tsx @@ -2,25 +2,38 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { ConnectMenu } from './ConnectMenu'; -vi.mock('@/store', () => ({ - useFlowStore: (selector: (state: { nodes: Array<{ id: string; data: Record }> }) => unknown) => selector({ - nodes: [ - { - id: 'asset-1', - data: { - label: 'Analytics Athena', - assetPresentation: 'icon', - assetProvider: 'aws', - assetCategory: 'Analytics', - archIconShapeId: 'analytics-athena', - }, +const mockStoreState: { nodes: Array<{ id: string; data: Record }> } = { + nodes: [ + { + id: 'asset-1', + data: { + label: 'Analytics Athena', + assetPresentation: 'icon', + assetProvider: 'aws', + assetCategory: 'Analytics', + archIconPackId: 'aws-official-starter-v1', + archIconShapeId: 'analytics-athena', }, - ], - }), + }, + ], +}; + +vi.mock('@/store', () => ({ + useFlowStore: (selector: (state: { nodes: Array<{ id: string; data: Record }> }) => unknown) => selector(mockStoreState), })); -vi.mock('@/services/shapeLibrary/providerCatalog', () => ({ - loadProviderCatalogSuggestions: vi.fn(async () => [ +vi.mock('@/services/shapeLibrary/providerCatalog', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadProviderShapePreview: vi.fn(async () => ({ + previewUrl: '/mock/glue.svg', + })), + }; +}); + +vi.mock('@/services/assetCatalog', () => ({ + loadDomainAssetSuggestions: vi.fn(async () => [ { id: 'aws-official-starter-v1:analytics-glue', category: 'aws', @@ -35,9 +48,6 @@ vi.mock('@/services/shapeLibrary/providerCatalog', () => ({ archIconShapeId: 'analytics-glue', }, ]), - loadProviderShapePreview: vi.fn(async () => ({ - previewUrl: '/mock/glue.svg', - })), })); vi.mock('react-i18next', () => ({ @@ -82,6 +92,20 @@ describe('ConnectMenu', () => { }); it('shows provider suggestions for asset nodes instead of generic shapes', async () => { + mockStoreState.nodes = [ + { + id: 'asset-1', + data: { + label: 'Analytics Athena', + assetPresentation: 'icon', + assetProvider: 'aws', + assetCategory: 'Analytics', + archIconPackId: 'aws-official-starter-v1', + archIconShapeId: 'analytics-athena', + }, + }, + ]; + render( { expect(screen.queryByText('connectMenu.process')).toBeNull(); }); + it('derives asset provider from pack metadata for older icon nodes', async () => { + mockStoreState.nodes = [ + { + id: 'asset-legacy', + data: { + label: 'Lambda', + assetPresentation: 'icon', + archIconPackId: 'aws-official-starter-v1', + archIconShapeId: 'compute-lambda', + }, + }, + ]; + + render( + + ); + + expect(await screen.findByRole('menuitem', { name: /Analytics Glue/i })).toBeTruthy(); + }); + it('surfaces contextual class creation first for class connectors', () => { const onSelect = vi.fn(); render( diff --git a/src/components/ConnectMenu.tsx b/src/components/ConnectMenu.tsx index 4629a318..28d8016b 100644 --- a/src/components/ConnectMenu.tsx +++ b/src/components/ConnectMenu.tsx @@ -9,6 +9,7 @@ import { getAssetCategoryDisplayName } from '@/services/assetPresentation'; import { loadProviderShapePreview } from '@/services/shapeLibrary/providerCatalog'; import type { ConnectedEdgePreset } from '@/hooks/edge-operations/utils'; import { useMenuKeyboardNavigation } from '@/hooks/useMenuKeyboardNavigation'; +import { normalizeNodeIconData } from '@/lib/nodeIconState'; import { type ConnectMenuOption, GenericConnectOptionsSection, @@ -96,13 +97,14 @@ export const ConnectMenu = ({ position, sourceId, sourceType, onSelect, onSelect const menuRef = React.useRef(null); const { onKeyDown } = useMenuKeyboardNavigation({ menuRef, onClose }); const sourceNode = useFlowStore((state) => state.nodes.find((node) => node.id === sourceId)); + const normalizedIconData = normalizeNodeIconData(sourceNode?.data); const isMindmapSource = isMindmapConnectorSource(sourceType); - const isAssetSource = sourceNode?.data?.assetPresentation === 'icon' - && typeof sourceNode.data?.assetProvider === 'string'; - const assetProvider = (sourceNode?.data?.assetProvider || null) as DomainLibraryCategory | null; - const assetCategory = typeof sourceNode?.data?.assetCategory === 'string' ? sourceNode.data.assetCategory : undefined; - const currentShapeId = typeof sourceNode?.data?.archIconShapeId === 'string' ? sourceNode.data.archIconShapeId : undefined; - const currentIconName = typeof sourceNode?.data?.icon === 'string' ? sourceNode.data.icon : undefined; + const isAssetSource = normalizedIconData?.assetPresentation === 'icon' + && typeof normalizedIconData.assetProvider === 'string'; + const assetProvider = (normalizedIconData?.assetProvider || null) as DomainLibraryCategory | null; + const assetCategory = typeof normalizedIconData?.assetCategory === 'string' ? normalizedIconData.assetCategory : undefined; + const currentShapeId = typeof normalizedIconData?.archIconShapeId === 'string' ? normalizedIconData.archIconShapeId : undefined; + const currentIconName = typeof normalizedIconData?.icon === 'string' ? normalizedIconData.icon : undefined; const providerItemsKey = isAssetSource && assetProvider ? `${assetProvider}:${assetCategory ?? 'all'}:${currentShapeId ?? currentIconName ?? 'all'}` : null; diff --git a/src/components/CustomEdge.tsx b/src/components/CustomEdge.tsx index b2744ec7..0e4a3779 100644 --- a/src/components/CustomEdge.tsx +++ b/src/components/CustomEdge.tsx @@ -4,10 +4,13 @@ import { ROLLOUT_FLAGS } from '@/config/rolloutFlags'; import type { LegacyEdgeProps } from '@/lib/reactflowCompat'; import type { EdgeData } from '@/lib/types'; import type { FlowEdge } from '@/lib/types'; +import type { NodeData } from '@/lib/types'; import { useCinematicExportState } from '@/context/CinematicExportContext'; import { CustomEdgeWrapper } from './custom-edge/CustomEdgeWrapper'; import { buildEdgePath } from './custom-edge/pathUtils'; import { shouldUseOrthogonalRelationRouting } from './custom-edge/relationRoutingSemantics'; +import { readMermaidImportedEdgeMetadata } from '@/services/mermaid/importProvenance'; +import { readMermaidImportedNodeMetadataFromData } from '@/services/mermaid/importProvenance'; function createEdgeRenderer(variant: 'bezier' | 'smoothstep' | 'step' | 'straight') { return function RenderEdge(props: LegacyEdgeProps): React.ReactElement { @@ -16,9 +19,14 @@ function createEdgeRenderer(variant: 'bezier' | 'smoothstep' | 'step' | 'straigh const allEdges = getEdges() as FlowEdge[]; const allNodes = getNodes(); const currentEdge = allEdges.find((edge) => edge.id === props.id) as FlowEdge | undefined; + const importedEdgeMetadata = currentEdge ? readMermaidImportedEdgeMetadata(currentEdge) : null; const relationSemanticsV1Enabled = ROLLOUT_FLAGS.relationSemanticsV1; const sourceNode = allNodes.find((node) => node.id === props.source); const targetNode = allNodes.find((node) => node.id === props.target); + const sourceIsImportedMermaidContainer = + readMermaidImportedNodeMetadataFromData(sourceNode?.data as NodeData | undefined)?.role === 'container'; + const targetIsImportedMermaidContainer = + readMermaidImportedNodeMetadataFromData(targetNode?.data as NodeData | undefined)?.role === 'container'; const forceOrthogonal = shouldUseOrthogonalRelationRouting( relationSemanticsV1Enabled, props.data, @@ -44,9 +52,22 @@ function createEdgeRenderer(variant: 'bezier' | 'smoothstep' | 'step' | 'straigh variant, { forceOrthogonal, + mermaidPreservedEndpoints: + importedEdgeMetadata?.hasFixedRoute === false, + mermaidSourceContainer: sourceIsImportedMermaidContainer, + mermaidTargetContainer: targetIsImportedMermaidContainer, elkPoints: props.data?.elkPoints as { x: number; y: number }[] | undefined, + importRoutePoints: props.data?.importRoutePoints as + | { x: number; y: number }[] + | undefined, + importRoutePath: props.data?.importRoutePath as string | undefined, mindmapBranchKind: props.data?.mindmapBranchKind as 'root' | 'branch' | undefined, - routingMode: props.data?.routingMode as 'auto' | 'elk' | 'manual' | undefined, + routingMode: props.data?.routingMode as + | 'auto' + | 'elk' + | 'manual' + | 'import-fixed' + | undefined, waypoints: props.data?.waypoints as { x: number; y: number }[] | undefined, waypoint: props.data?.waypoint as { x: number; y: number } | undefined, } diff --git a/src/components/CustomNode.tsx b/src/components/CustomNode.tsx index 36451fca..53e9aa51 100644 --- a/src/components/CustomNode.tsx +++ b/src/components/CustomNode.tsx @@ -17,6 +17,7 @@ import { NodeShapeSVG } from './NodeShapeSVG'; import { DiffBadge, LintViolationBadge } from './NodeBadges'; import { IconAssetNodeBody } from './IconAssetNodeBody'; import { CustomNodeContent } from './CustomNodeContent'; +import { readMermaidImportedNodeMetadataFromData } from '@/services/mermaid/importProvenance'; import { type NodeShape, COMPLEX_SHAPES, @@ -24,17 +25,52 @@ import { NEEDS_SQUARE_ASPECT, COMPLEX_SHAPE_PADDING, getNodeDefaults, + getNumericNodeDimension, getMinNodeSize, toCssSize, getNodeBorderRadius, fontSizeClassFor, } from './nodeHelpers'; +function getMermaidImportedFontSize(nodeHeightPx: number | undefined): number { + if (typeof nodeHeightPx !== 'number') { + return 15; + } + + if (nodeHeightPx <= 56) { + return 14; + } + + if (nodeHeightPx >= 96) { + return 16; + } + + return 15; +} + +function getMermaidImportedContentPadding(nodeHeightPx: number | undefined): string { + if (typeof nodeHeightPx !== 'number') { + return '0.6rem 0.75rem'; + } + + if (nodeHeightPx <= 40) { + return '0.4rem 0.6rem'; + } + + if (nodeHeightPx <= 60) { + return '0.5rem 0.7rem'; + } + + return '0.65rem 0.9rem'; +} + function CustomNode(props: LegacyNodeProps): React.ReactElement { const { id, data, type, selected } = props; const explicitNodeStyle = (props as { style?: React.CSSProperties }).style; const explicitWidth = data.width ?? explicitNodeStyle?.width; const explicitHeight = data.height ?? explicitNodeStyle?.height; + const explicitWidthPx = getNumericNodeDimension(explicitWidth); + const explicitHeightPx = getNumericNodeDimension(explicitHeight); const measuredHeight = (props as { height?: number }).height; const shiftHeld = useShiftHeld(Boolean(selected)); const resolvedAssetIconUrl = useProviderShapePreview( @@ -73,8 +109,10 @@ function CustomNode(props: LegacyNodeProps): React.ReactElement { const hasIcon = Boolean(iconName) || Boolean(data.customIconUrl); const hasLabel = Boolean(data.label?.trim()); const hasSubLabel = Boolean(data.subLabel); + const mermaidImportedNodeMetadata = readMermaidImportedNodeMetadataFromData(data); + const isMermaidImportedLeaf = mermaidImportedNodeMetadata?.role === 'leaf'; const isComplexShape = COMPLEX_SHAPES.includes(activeShape); - const { minWidth, minHeight } = getMinNodeSize(activeShape); + const { minWidth: baseMinWidth, minHeight: baseMinHeight } = getMinNodeSize(activeShape); const contentMinHeight = !isComplexShape ? hasIcon && hasSubLabel ? 128 @@ -83,11 +121,18 @@ function CustomNode(props: LegacyNodeProps): React.ReactElement { : hasSubLabel ? 96 : 84 - : minHeight; - const effectiveMinHeight = Math.max(minHeight, contentMinHeight); - const nodeHeightPx = typeof measuredHeight === 'number' ? measuredHeight : undefined; + : baseMinHeight; + const minWidth = isMermaidImportedLeaf ? explicitWidthPx ?? baseMinWidth : baseMinWidth; + const effectiveMinHeight = isMermaidImportedLeaf + ? explicitHeightPx ?? baseMinHeight + : Math.max(baseMinHeight, contentMinHeight); + const nodeHeightPx = typeof measuredHeight === 'number' ? measuredHeight : explicitHeightPx; const isCompactNode = typeof nodeHeightPx === 'number' && nodeHeightPx < effectiveMinHeight + 8; - const contentPadding = isCompactNode ? '0.5rem' : designSystem.components.node.padding; + const contentPadding = isMermaidImportedLeaf + ? getMermaidImportedContentPadding(nodeHeightPx) + : isCompactNode + ? '0.5rem' + : designSystem.components.node.padding; const labelEdit = useInlineNodeTextEdit(id, 'label', data.label || '', { multiline: true }); const subLabelEdit = useInlineNodeTextEdit(id, 'subLabel', data.subLabel || ''); const connectionHandleClass = @@ -99,11 +144,13 @@ function CustomNode(props: LegacyNodeProps): React.ReactElement { data.assetPresentation === 'icon' && (Boolean(resolvedAssetIconUrl) || Boolean(activeIconKey) || Boolean(data.archIconPackId)); - const labelDisplayValue = hasLabel ? ( - - ) : showEmptyLabelPrompt ? ( - {emptyLabelPrompt} - ) : null; + const labelDisplayValue = hasLabel + ? isMermaidImportedLeaf + ? {data.label} + : + : showEmptyLabelPrompt + ? {emptyLabelPrompt} + : null; const needsSquareAspect = NEEDS_SQUARE_ASPECT.has(activeShape); const selectionRing = @@ -151,13 +198,23 @@ function CustomNode(props: LegacyNodeProps): React.ReactElement { ); } + const importedFontFamilyStyle = + isMermaidImportedLeaf && !data.fontFamily + ? { fontFamily: designSystem.typography.fontFamily } + : {}; + const importedFontSizeStyle = + !data.fontSize && isMermaidImportedLeaf + ? { fontSize: `${getMermaidImportedFontSize(nodeHeightPx)}px` } + : {}; const textProps = { ...fontSizeStyle, + ...importedFontSizeStyle, ...labelFontFamilyStyle, + ...importedFontFamilyStyle, color: visualStyle.text, - fontWeight: data.fontWeight || '600', + fontWeight: data.fontWeight || (isMermaidImportedLeaf ? '500' : '600'), fontStyle: data.fontStyle || 'normal', - lineHeight: 1.2, + lineHeight: isMermaidImportedLeaf ? 1.1 : 1.2, }; const subTextProps = { ...subLabelFontSizeStyle, diff --git a/src/components/DesignSystem.integration.test.tsx b/src/components/DesignSystem.integration.test.tsx index 8e7bcbaf..3d218a7f 100644 --- a/src/components/DesignSystem.integration.test.tsx +++ b/src/components/DesignSystem.integration.test.tsx @@ -4,8 +4,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Position } from '@/lib/reactflowCompat'; import type { DesignSystem } from '@/lib/types'; import { DEFAULT_DESIGN_SYSTEM, useFlowStore } from '@/store'; +import { attachMermaidImportedNodeMetadata } from '@/services/mermaid/importProvenance'; import CustomNode from './CustomNode'; import { CustomSmoothStepEdge } from './CustomEdge'; +import SectionNode from './SectionNode'; import { DEFAULT_EDGE_OPTIONS } from '@/constants'; vi.mock('react-i18next', () => ({ @@ -55,6 +57,7 @@ vi.mock('@/lib/reactflowCompat', async (importOriginal) => { setEdges: vi.fn(), screenToFlowPosition: ({ x, y }: { x: number; y: number }) => ({ x, y }), }), + useNodes: () => [], useViewport: () => ({ zoom: 1 }), }; }); @@ -243,4 +246,95 @@ describe('Design System integration', () => { expect(diagnosticsNode?.style.width).toBe('100%'); expect(diagnosticsNode?.style.height).toBe(''); }); + + it('honors imported Mermaid node geometry instead of generic canvas minimums', () => { + const importedNodeData = attachMermaidImportedNodeMetadata( + { + id: 'decision-1', + type: 'decision', + position: { x: 0, y: 0 }, + data: { label: 'Approved?' }, + } as const, + { + role: 'leaf', + source: 'official-flowchart', + fidelity: 'renderer-backed', + } + ).data; + + const { container } = render( + + ); + + const diagnosticsNode = container.querySelector('[data-transform-diagnostics="1"]') as HTMLElement | null; + expect(diagnosticsNode).not.toBeNull(); + expect(diagnosticsNode?.style.minWidth).toBe('150px'); + expect(diagnosticsNode?.style.minHeight).toBe('70px'); + expect(diagnosticsNode?.style.width).toBe('150px'); + expect(diagnosticsNode?.style.height).toBe('70px'); + expect(diagnosticsNode?.getAttribute('data-transform-compact')).toBe('1'); + const importedLabelStyle = screen.getByText('Approved?').parentElement?.getAttribute('style') ?? ''; + expect(importedLabelStyle).toContain('font-family:'); + // Imported nodes now use the design system font for visual consistency + expect(importedLabelStyle).not.toContain('Trebuchet MS'); + expect(importedLabelStyle).toContain('line-height: 1.1;'); + }); + + it('honors imported Mermaid section geometry instead of generic section minimums', () => { + const importedSectionData = attachMermaidImportedNodeMetadata( + { + id: 'payments', + type: 'section', + position: { x: 0, y: 0 }, + data: { label: 'Payments' }, + } as const, + { + role: 'container', + source: 'official-flowchart', + fidelity: 'renderer-backed', + } + ).data; + + const { container } = render( + + ); + + const sectionFrame = Array.from(container.querySelectorAll('div')).find( + (element) => element instanceof HTMLElement + && element.style.minWidth === '260px' + && element.style.minHeight === '180px' + ) as HTMLElement | undefined; + + expect(sectionFrame).toBeTruthy(); + expect(sectionFrame?.getAttribute('data-section-render-variant')).toBe('mermaid-import'); + expect(screen.getByText('Imported')).toBeTruthy(); + expect(screen.queryByText(/items?$/)).toBeNull(); + expect(screen.getByText('Payments').parentElement?.getAttribute('style')).toContain('top: 8px;'); + }); }); diff --git a/src/components/ExportMenuPanel.tsx b/src/components/ExportMenuPanel.tsx index 35ea77d7..d71d820b 100644 --- a/src/components/ExportMenuPanel.tsx +++ b/src/components/ExportMenuPanel.tsx @@ -11,10 +11,8 @@ import { GitBranch, Image, Share2, - Wand2, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { APP_NAME } from '@/lib/brand'; import { type CinematicExportResolution, type CinematicExportSpeed, @@ -177,16 +175,6 @@ export function ExportMenuPanel({ Icon: FileJson, actions: ['download', 'copy'], }, - { - key: 'openflow', - label: t('export.openflowdslLabel', { - appName: APP_NAME, - defaultValue: `${APP_NAME} DSL`, - }), - hint: t('export.actionCopy', 'Copy'), - Icon: Wand2, - actions: ['download', 'copy'], - }, { key: 'mermaid', label: t('export.mermaid', 'Mermaid'), diff --git a/src/components/FlowCanvas.tsx b/src/components/FlowCanvas.tsx index 6d38893b..a46fae43 100644 --- a/src/components/FlowCanvas.tsx +++ b/src/components/FlowCanvas.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useShallow } from 'zustand/react/shallow'; import { useReactFlow, toFlowNode } from '@/lib/reactflowCompat'; @@ -26,6 +26,12 @@ import { useSelectionActions } from '@/store/selectionHooks'; import { useTabActions, useActiveTabId } from '@/store/tabHooks'; import { useCanvasViewSettings } from '@/store/viewHooks'; import { useMermaidDiagnosticsActions } from '@/store/selectionHooks'; +import { + clearImportLayoutMetadata, + isImportPendingLayoutNode, + readImportLayoutMetadata, +} from '@/services/importLayoutMetadata'; +import { composeDiagramForDisplay } from '@/services/composeDiagramForDisplay'; interface FlowCanvasProps { recordHistory: () => void; @@ -52,6 +58,7 @@ export const FlowCanvas: React.FC = ({ largeGraphSafetyMode, largeGraphSafetyProfile, architectureStrictMode, + mermaidImportMode, } = useCanvasViewSettings(); const { layers } = useFlowStore( useShallow((state) => ({ @@ -76,6 +83,7 @@ export const FlowCanvas: React.FC = ({ const reactFlowWrapper = useRef(null); const lastInteractionScreenPositionRef = useRef<{ x: number; y: number } | null>(null); const connectMenuSetterRef = useRef<((value: ConnectMenuState | null) => void) | null>(null); + const importStabilizationSignatureRef = useRef(null); const { screenToFlowPosition, fitView } = useReactFlow(); const clearPaneSelection = useCallback((): void => { @@ -240,6 +248,7 @@ export const FlowCanvas: React.FC = ({ const { handleCanvasPaste } = useFlowCanvasPaste({ architectureStrictMode, + mermaidImportMode, activeTabId, fitView, updateTab, @@ -266,6 +275,122 @@ export const FlowCanvas: React.FC = ({ }; }, [interactionLowDetailModeActive]); + // Stable signal: changes only when import-pending node IDs or their measured + // state change โ€” not on every node array reference update. + const importStabilizationKey = useMemo(() => { + const pending = nodes.filter(isImportPendingLayoutNode); + if (pending.length === 0) return null; + return pending + .map((n) => { + const m = (n as FlowNode & { measured?: { width?: number; height?: number } }).measured; + return `${n.id}:${m?.width ?? '?'}x${m?.height ?? '?'}`; + }) + .join('|'); + }, [nodes]); + + useEffect(() => { + if (!importStabilizationKey) { + importStabilizationSignatureRef.current = null; + return; + } + + const pendingImportNodes = nodes.filter(isImportPendingLayoutNode); + const metadata = readImportLayoutMetadata(pendingImportNodes); + if (!metadata || importStabilizationSignatureRef.current === metadata.signature) { + return; + } + + const measurableNodes = pendingImportNodes.filter((node) => node.type !== 'section'); + const allMeasured = + measurableNodes.length > 0 + && measurableNodes.every((node) => { + const m = (node as FlowNode & { measured?: { width?: number; height?: number } }).measured; + return typeof m?.width === 'number' && typeof m?.height === 'number'; + }); + + if (!allMeasured) { + return; + } + + importStabilizationSignatureRef.current = metadata.signature; + + const runConvergenceLoop = async (signature: string): Promise => { + const { clearLayoutCache } = await import('@/services/elkLayout'); + const { assignSmartHandles } = await import('@/services/smartEdgeRouting'); + + // Up to 3 layout passes until node positions converge within 1px. + // Mermaid.js does the same โ€” first pass uses estimated sizes, subsequent + // passes use React Flow's measured dimensions for precision. + const MAX_PASSES = 3; + const CONVERGENCE_THRESHOLD_PX = 1; + + let prevPositions: Map | null = null; + + for (let pass = 0; pass < MAX_PASSES; pass++) { + const state = useFlowStore.getState(); + const currentMetadata = readImportLayoutMetadata(state.nodes); + if (!currentMetadata || currentMetadata.signature !== signature) return; + + clearLayoutCache(); + const { nodes: layoutedNodes, edges: layoutedEdges } = await composeDiagramForDisplay( + state.nodes, + state.edges, + { + direction: currentMetadata.direction, + spacing: currentMetadata.spacing, + contentDensity: currentMetadata.contentDensity, + diagramType: currentMetadata.diagramType, + source: 'import', + } + ); + + const nextPositions = new Map(layoutedNodes.map((n) => [n.id, n.position])); + + const converged = + prevPositions !== null && + layoutedNodes.every((n) => { + const prev = prevPositions!.get(n.id); + return ( + prev !== undefined && + Math.abs(n.position.x - prev.x) <= CONVERGENCE_THRESHOLD_PX && + Math.abs(n.position.y - prev.y) <= CONVERGENCE_THRESHOLD_PX + ); + }); + + prevPositions = nextPositions; + + const latestMetadata = readImportLayoutMetadata(useFlowStore.getState().nodes); + if (!latestMetadata || latestMetadata.signature !== signature) return; + + const smartEdges = assignSmartHandles(layoutedNodes, layoutedEdges); + const finalNodes = converged || pass === MAX_PASSES - 1 + ? clearImportLayoutMetadata(layoutedNodes) + : layoutedNodes; + + setNodes(finalNodes); + setEdges(smartEdges); + + if (converged) break; + + // Yield to React to render and measure the updated nodes before next pass. + await new Promise((resolve) => { window.setTimeout(resolve, 60); }); + } + + fitView({ duration: 500, padding: 0.2 }); + }; + + const timer = window.setTimeout(() => { + void runConvergenceLoop(metadata.signature).finally(() => { + if (importStabilizationSignatureRef.current === metadata.signature) { + importStabilizationSignatureRef.current = null; + } + }); + // Wait for React to render and measure nodes before first pass. + }, 60); + + return () => window.clearTimeout(timer); + }, [fitView, importStabilizationKey, nodes, setEdges, setNodes]); + const selectedNodeCount = nodes.filter((node) => node.selected).length; const selectedEdgeCount = edges.filter((edge) => edge.selected).length; const selectionAnnouncement = diff --git a/src/components/FlowEditor.test.tsx b/src/components/FlowEditor.test.tsx new file mode 100644 index 00000000..f5587253 --- /dev/null +++ b/src/components/FlowEditor.test.tsx @@ -0,0 +1,230 @@ +import type { ReactNode } from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { FlowEditor } from './FlowEditor'; +import type { ImportFidelityReport } from '@/services/importFidelity'; + +const openStudioCode = vi.fn(); +const importRecoveryDialogMock = vi.fn(); +const useMermaidDiagnosticsMock = vi.fn(); +const useFlowEditorScreenModelMock = vi.fn(); +const setNodesMock = vi.fn(); +const setEdgesMock = vi.fn(); +const updateTabMock = vi.fn(); +const setMermaidDiagnosticsMock = vi.fn(); +const clearMermaidDiagnosticsMock = vi.fn(); + +vi.mock('./FlowCanvas', () => ({ + FlowCanvas: () =>
FlowCanvas
, +})); + +vi.mock('./CinematicExportOverlay', () => ({ + CinematicExportOverlay: () =>
CinematicExportOverlay
, +})); + +vi.mock('./flow-editor/FlowEditorChrome', () => ({ + FlowEditorChrome: () =>
FlowEditorChrome
, +})); + +vi.mock('@/context/CinematicExportContext', () => ({ + useCinematicExportState: () => ({ active: false, backgroundMode: 'light' }), +})); + +vi.mock('@/context/ArchitectureLintContext', () => ({ + ArchitectureLintProvider: ({ children }: { children: ReactNode }) => <>{children}, +})); + +vi.mock('@/context/DiagramDiffContext', () => ({ + DiagramDiffProvider: ({ children }: { children: ReactNode }) => <>{children}, +})); + +vi.mock('@/components/ShareEmbedModal', () => ({ + ShareEmbedModal: () =>
ShareEmbedModal
, +})); + +vi.mock('@/components/ImportRecoveryDialog', () => ({ + ImportRecoveryDialog: (props: unknown) => { + importRecoveryDialogMock(props); + return
ImportRecoveryDialog
; + }, +})); + +vi.mock('@/store/selectionHooks', () => ({ + useMermaidDiagnostics: () => useMermaidDiagnosticsMock(), + useMermaidDiagnosticsActions: () => ({ + setMermaidDiagnostics: setMermaidDiagnosticsMock, + clearMermaidDiagnostics: clearMermaidDiagnosticsMock, + }), +})); + +vi.mock('@/store/canvasHooks', () => ({ + useCanvasActions: () => ({ + setNodes: setNodesMock, + setEdges: setEdgesMock, + }), +})); + +vi.mock('@/store/tabHooks', () => ({ + useTabActions: () => ({ + updateTab: updateTabMock, + }), +})); + +vi.mock('./flow-editor/useFlowEditorScreenModel', () => ({ + useFlowEditorScreenModel: () => useFlowEditorScreenModelMock(), +})); + +function createMermaidDiagnostics() { + return { + source: 'code', + diagramType: 'flowchart', + importState: 'editable_partial', + statusLabel: 'Ready with warnings', + statusDetail: 'Original source is preserved for recovery.', + originalSource: 'flowchart TD\nA-->B', + diagnostics: [{ message: 'warning' }], + updatedAt: 1, + }; +} + +function createMermaidLayoutWarningDiagnostics() { + return { + source: 'code', + diagramType: 'flowchart', + importState: 'editable_full', + layoutMode: 'mermaid_preserved_partial', + layoutFallbackReason: 'matched 1/2 official flowchart edge routes', + statusLabel: 'Ready to apply', + statusDetail: '2 nodes, 1 edges ยท Partial Mermaid layout preserved', + originalSource: 'flowchart LR\nA-->B', + diagnostics: [], + updatedAt: 1, + }; +} + +function createFlowEditorScreenModel( + importRecoveryState: { + fileName: string; + report: Pick; + } | null = null +) { + return { + nodes: [], + edges: [], + pages: [], + activePageId: null, + viewSettings: { lintRules: '{}' }, + diffBaseline: null, + setDiffBaseline: vi.fn(), + recordHistory: vi.fn(), + isSelectMode: true, + reactFlowWrapper: { current: null }, + fileInputRef: { current: null }, + handleImportJSON: vi.fn(), + onFileImport: vi.fn(), + importRecoveryState, + dismissImportRecovery: vi.fn(), + shareViewerUrl: null, + clearShareViewerUrl: vi.fn(), + collaborationEnabled: false, + remotePresence: [], + collaborationNodePositions: {}, + isLayouting: false, + flowEditorController: { + shouldRenderPanels: true, + handleCanvasEntityIntent: vi.fn(), + openStudioCode, + panels: {}, + chrome: { + topNav: {}, + playback: {}, + toolbar: {}, + emptyState: {}, + }, + }, + t: (key: string) => key, + }; +} + +describe('FlowEditor', () => { + beforeEach(() => { + openStudioCode.mockReset(); + importRecoveryDialogMock.mockReset(); + setNodesMock.mockReset(); + setEdgesMock.mockReset(); + updateTabMock.mockReset(); + setMermaidDiagnosticsMock.mockReset(); + clearMermaidDiagnosticsMock.mockReset(); + useMermaidDiagnosticsMock.mockReturnValue(createMermaidDiagnostics()); + useFlowEditorScreenModelMock.mockReturnValue(createFlowEditorScreenModel()); + }); + + it('opens Mermaid code recovery from the shell diagnostics banner', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Open Mermaid code' })); + expect(openStudioCode).toHaveBeenCalledWith('mermaid'); + }); + + it('passes Mermaid recovery action into the import recovery dialog when source is preserved', () => { + useMermaidDiagnosticsMock.mockReturnValue(null); + useFlowEditorScreenModelMock.mockReturnValue( + createFlowEditorScreenModel({ + fileName: 'unsupported.mmd', + report: { + source: 'mermaid', + importState: 'unsupported_family', + originalSource: 'gitGraph\ncommit id: "a1"', + }, + }) + ); + + render(); + + expect(importRecoveryDialogMock).toHaveBeenCalled(); + const props = importRecoveryDialogMock.mock.calls[0][0] as { + actionLabel?: string; + onAction?: () => void; + }; + expect(props.actionLabel).toBe('Open Mermaid code'); + + props.onAction?.(); + expect(openStudioCode).toHaveBeenCalledWith('mermaid'); + }); + + it('passes Mermaid recovery action into the import recovery dialog when parsing is editable_full but report layout degraded', () => { + useMermaidDiagnosticsMock.mockReturnValue(null); + useFlowEditorScreenModelMock.mockReturnValue( + createFlowEditorScreenModel({ + fileName: 'layout-warning.mmd', + report: { + source: 'mermaid', + importState: 'editable_full', + layoutMode: 'mermaid_preserved_partial', + originalSource: 'flowchart LR\nA-->B', + }, + }) + ); + + render(); + + expect(importRecoveryDialogMock).toHaveBeenCalled(); + const props = importRecoveryDialogMock.mock.calls[0][0] as { + actionLabel?: string; + onAction?: () => void; + }; + expect(props.actionLabel).toBe('Open Mermaid code'); + + props.onAction?.(); + expect(openStudioCode).toHaveBeenCalledWith('mermaid'); + }); + + it('still offers Mermaid code recovery when parsing is editable_full but layout fidelity degraded', () => { + useMermaidDiagnosticsMock.mockReturnValue(createMermaidLayoutWarningDiagnostics()); + + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Open Mermaid code' })); + expect(openStudioCode).toHaveBeenCalledWith('mermaid'); + }); +}); diff --git a/src/components/FlowEditor.tsx b/src/components/FlowEditor.tsx index 886e617c..5335b13a 100644 --- a/src/components/FlowEditor.tsx +++ b/src/components/FlowEditor.tsx @@ -9,7 +9,18 @@ import { useCinematicExportState } from '@/context/CinematicExportContext'; import { DiagramDiffProvider } from '@/context/DiagramDiffContext'; import { ShareEmbedModal } from '@/components/ShareEmbedModal'; import { ImportRecoveryDialog } from '@/components/ImportRecoveryDialog'; +import { MermaidDiagnosticsBanner } from '@/components/MermaidDiagnosticsBanner'; +import { canRecoverMermaidSource as canRecoverMermaidSourceFromState } from '@/services/mermaid/recoveryPresentation'; import { resolveCinematicExportTheme } from '@/services/export/cinematicExportTheme'; +import { useMermaidDiagnostics } from '@/store/selectionHooks'; +import { useCanvasActions } from '@/store/canvasHooks'; +import { useTabActions } from '@/store/tabHooks'; +import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType'; +import { importMermaidToCanvas } from '@/services/mermaid/rendererFirstImport'; +import { resolveLayoutDirection } from '@/components/flow-canvas/pasteHelpers'; +import { buildMermaidDiagnosticsSnapshot } from '@/services/mermaid/diagnosticsSnapshot'; +import { normalizeParseDiagnostics } from '@/services/mermaid/diagnosticFormatting'; +import { useMermaidDiagnosticsActions } from '@/store/selectionHooks'; interface FlowEditorProps { onGoHome: () => void; @@ -17,6 +28,10 @@ interface FlowEditorProps { export function FlowEditor({ onGoHome }: FlowEditorProps) { const cinematicExportState = useCinematicExportState(); + const mermaidDiagnostics = useMermaidDiagnostics(); + const { setNodes, setEdges } = useCanvasActions(); + const { updateTab } = useTabActions(); + const { setMermaidDiagnostics, clearMermaidDiagnostics } = useMermaidDiagnosticsActions(); const { nodes, edges, @@ -43,6 +58,92 @@ export function FlowEditor({ onGoHome }: FlowEditorProps) { t, } = useFlowEditorScreenModel({ onGoHome }); const cinematicExportTheme = resolveCinematicExportTheme(cinematicExportState.backgroundMode); + const mermaidRecoverySource = importRecoveryState?.report.source === 'mermaid' + ? (importRecoveryState.report.originalSource ?? mermaidDiagnostics?.originalSource) + : mermaidDiagnostics?.originalSource; + const canRecoverMermaidSource = importRecoveryState?.report.source === 'mermaid' + ? canRecoverMermaidSourceFromState({ + originalSource: mermaidRecoverySource, + importState: importRecoveryState.report.importState, + layoutMode: importRecoveryState.report.layoutMode, + }) + : canRecoverMermaidSourceFromState({ + originalSource: mermaidRecoverySource, + importState: mermaidDiagnostics?.importState, + layoutMode: mermaidDiagnostics?.layoutMode, + }); + + const handleConvertMermaidToEditable = React.useCallback(async () => { + if (!mermaidRecoverySource) { + return; + } + + const parsed = parseMermaidByType(mermaidRecoverySource, { + architectureStrictMode: viewSettings.architectureStrictMode, + }); + const diagnostics = normalizeParseDiagnostics(parsed.diagnostics); + + if (parsed.error) { + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'import', + diagramType: parsed.diagramType, + importState: parsed.importState, + originalSource: mermaidRecoverySource, + diagnostics, + error: parsed.error, + }) + ); + return; + } + + const editableImport = await importMermaidToCanvas({ + parsed, + source: mermaidRecoverySource, + importMode: 'native_editable', + layout: { + direction: resolveLayoutDirection(parsed), + spacing: 'normal', + contentDensity: 'balanced', + }, + }); + + recordHistory(); + setNodes(editableImport.nodes); + setEdges(editableImport.edges); + if (parsed.diagramType) { + updateTab(activePageId, { diagramType: parsed.diagramType }); + } + + if (diagnostics.length > 0 || editableImport.visualMode !== 'editable_exact') { + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'import', + diagramType: parsed.diagramType, + importState: parsed.importState, + originalSource: mermaidRecoverySource, + diagnostics, + nodeCount: editableImport.nodes.length, + edgeCount: editableImport.edges.length, + layoutMode: editableImport.layoutMode, + visualMode: editableImport.visualMode, + layoutFallbackReason: editableImport.layoutFallbackReason, + }) + ); + } else { + clearMermaidDiagnostics(); + } + }, [ + activePageId, + clearMermaidDiagnostics, + mermaidRecoverySource, + recordHistory, + setEdges, + setMermaidDiagnostics, + setNodes, + updateTab, + viewSettings.architectureStrictMode, + ]); return ( + {mermaidDiagnostics ? ( +
+
+ flowEditorController.openStudioCode('mermaid') + : undefined + } + /> +
+
+ ) : null} flowEditorController.openStudioCode('mermaid') + : undefined + } /> ) : null} diff --git a/src/components/ImportRecoveryDialog.test.tsx b/src/components/ImportRecoveryDialog.test.tsx index c7a0d1e6..acf4a72d 100644 --- a/src/components/ImportRecoveryDialog.test.tsx +++ b/src/components/ImportRecoveryDialog.test.tsx @@ -35,6 +35,60 @@ function createReport(): ImportFidelityReport { }; } +function createMermaidReport(): ImportFidelityReport { + return { + id: 'import-2', + source: 'mermaid', + importState: 'unsupported_family', + layoutMode: 'elk_fallback', + layoutFallbackReason: 'Mermaid SVG extraction unavailable', + originalSource: 'gitGraph\ncommit id: "a1"', + timestamp: '2026-03-30T00:00:00.000Z', + status: 'failed', + nodeCount: 0, + edgeCount: 0, + elapsedMs: 11, + issues: [ + { + code: 'UNSUP-001', + severity: 'error', + message: 'Mermaid "gitGraph" is not supported yet in editable mode.', + }, + ], + summary: { + warningCount: 0, + errorCount: 1, + }, + }; +} + +function createMermaidLayoutWarningReport(): ImportFidelityReport { + return { + id: 'import-3', + source: 'mermaid', + importState: 'editable_full', + layoutMode: 'mermaid_preserved_partial', + layoutFallbackReason: 'matched 1/2 official flowchart edge routes', + originalSource: 'flowchart LR\nA-->B', + timestamp: '2026-03-30T00:00:00.000Z', + status: 'success_with_warnings', + nodeCount: 2, + edgeCount: 1, + elapsedMs: 11, + issues: [ + { + code: 'MERMAID_LAYOUT_PRESERVED', + severity: 'warning', + message: 'Partial Mermaid layout preserved: matched 1/2 official flowchart edge routes', + }, + ], + summary: { + warningCount: 1, + errorCount: 0, + }, + }; +} + describe('ImportRecoveryDialog', () => { it('renders import issues and invokes retry/close actions', () => { const onRetry = vi.fn(); @@ -60,4 +114,52 @@ describe('ImportRecoveryDialog', () => { fireEvent.click(screen.getByRole('button', { name: /Dismiss/i })); expect(onClose).toHaveBeenCalledTimes(1); }); + + it('renders Mermaid-specific recovery status and guidance', () => { + render( + + ); + + expect(screen.getByText('Unsupported Mermaid family')).toBeTruthy(); + expect(screen.getByText('ELK fallback')).toBeTruthy(); + expect(screen.getByText(/ELK fallback was used/i)).toBeTruthy(); + expect(screen.getByText(/Original Mermaid source is preserved/i)).toBeTruthy(); + }); + + it('renders and triggers an optional recovery action', () => { + const onAction = vi.fn(); + + render( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Open Mermaid code' })); + expect(onAction).toHaveBeenCalledTimes(1); + }); + + it('shows warning-grade Mermaid status when layout degraded despite editable_full parsing', () => { + render( + + ); + + expect(screen.getByText('Ready with warnings')).toBeTruthy(); + expect(screen.getByText('Preserved partial Mermaid layout')).toBeTruthy(); + }); }); diff --git a/src/components/ImportRecoveryDialog.tsx b/src/components/ImportRecoveryDialog.tsx index f898b363..a2caab95 100644 --- a/src/components/ImportRecoveryDialog.tsx +++ b/src/components/ImportRecoveryDialog.tsx @@ -1,7 +1,11 @@ import React, { useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { AlertTriangle, FileWarning, RefreshCcw, X } from 'lucide-react'; -import type { ImportFidelityReport } from '@/services/importFidelity'; +import { + getImportRecoveryGuidance, + type ImportFidelityReport, +} from '@/services/importFidelity'; +import { getMermaidStatusLabel } from '@/services/mermaid/importStatePresentation'; import { MODAL_PANEL_CLASS, SECTION_CARD_CLASS, SECTION_SURFACE_CLASS, STATUS_SURFACE_CLASS } from '@/lib/designTokens'; import { Button } from './ui/Button'; @@ -10,6 +14,8 @@ interface ImportRecoveryDialogProps { report: ImportFidelityReport; onRetry: () => void; onClose: () => void; + actionLabel?: string; + onAction?: () => void; } function formatSourceLabel(source: ImportFidelityReport['source']): string { @@ -20,15 +26,47 @@ function formatSourceLabel(source: ImportFidelityReport['source']): string { return source.toUpperCase(); } +function formatLayoutLabel(report: ImportFidelityReport): string | null { + if (report.source !== 'mermaid' || !report.layoutMode) { + return null; + } + + if (report.layoutMode === 'mermaid_exact') { + return 'Exact Mermaid layout'; + } + if (report.layoutMode === 'mermaid_preserved_partial') { + return 'Preserved partial Mermaid layout'; + } + if (report.layoutMode === 'mermaid_partial') { + return 'Partial Mermaid extraction with ELK'; + } + if (report.layoutMode === 'elk_fallback') { + return 'ELK fallback'; + } + + return null; +} + export function ImportRecoveryDialog({ fileName, report, onRetry, onClose, + actionLabel, + onAction, }: ImportRecoveryDialogProps): React.ReactElement | null { const closeButtonRef = useRef(null); const visibleIssues = report.issues.slice(0, 3); const remainingIssueCount = Math.max(0, report.issues.length - visibleIssues.length); + const mermaidStateLabel = + report.source === 'mermaid' + ? getMermaidStatusLabel({ + importState: report.importState, + layoutMode: report.layoutMode, + }) + : null; + const layoutLabel = formatLayoutLabel(report); + const recoveryGuidance = getImportRecoveryGuidance(report); useEffect(() => { closeButtonRef.current?.focus(); @@ -78,11 +116,23 @@ export function ImportRecoveryDialog({
-
+
Source
{formatSourceLabel(report.source)}
+ {mermaidStateLabel ? ( +
+
Status
+
{mermaidStateLabel}
+
+ ) : null} + {layoutLabel ? ( +
+
Layout
+
{layoutLabel}
+
+ ) : null}
Errors
{report.summary.errorCount}
@@ -124,10 +174,20 @@ export function ImportRecoveryDialog({
- If this file came from another tool, try exporting a plain JSON/OpenFlowKit file again or remove unsupported metadata before retrying. + {recoveryGuidance}
+ {report.source === 'mermaid' && report.originalSource ? ( +
+ Original Mermaid source is preserved for recovery. Open Mermaid code to continue editing safely. +
+ ) : null}
+ {actionLabel && onAction ? ( + + ) : null} diff --git a/src/components/MermaidDiagnosticsBanner.test.tsx b/src/components/MermaidDiagnosticsBanner.test.tsx new file mode 100644 index 00000000..14358542 --- /dev/null +++ b/src/components/MermaidDiagnosticsBanner.test.tsx @@ -0,0 +1,71 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { MermaidDiagnosticsBanner } from './MermaidDiagnosticsBanner'; + +describe('MermaidDiagnosticsBanner', () => { + it('renders status and detail for warning snapshots', () => { + render( + B', + diagnostics: [{ message: 'warning' }], + updatedAt: 1, + }} + /> + ); + + expect(screen.getByText('Ready with warnings')).toBeTruthy(); + expect(screen.getByText('2 nodes, 1 edges, partial editability')).toBeTruthy(); + expect(screen.getByText(/Original Mermaid source is preserved/i)).toBeTruthy(); + }); + + it('renders and triggers the recovery action when provided', () => { + const onAction = vi.fn(); + + render( + B', + diagnostics: [{ message: 'unsupported' }], + updatedAt: 1, + }} + actionLabel="Open Mermaid code" + onAction={onAction} + /> + ); + + fireEvent.click(screen.getByRole('button', { name: 'Open Mermaid code' })); + expect(onAction).toHaveBeenCalledTimes(1); + }); + + it('keeps the Mermaid recovery hint visible when layout fidelity degrades despite editable_full parsing', () => { + render( + B', + diagnostics: [], + updatedAt: 1, + }} + /> + ); + + expect(screen.getByText(/Original Mermaid source is preserved/i)).toBeTruthy(); + }); +}); diff --git a/src/components/MermaidDiagnosticsBanner.tsx b/src/components/MermaidDiagnosticsBanner.tsx new file mode 100644 index 00000000..8119f225 --- /dev/null +++ b/src/components/MermaidDiagnosticsBanner.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { AlertCircle } from 'lucide-react'; +import type { MermaidDiagnosticsSnapshot } from '@/store/types'; +import { canRecoverMermaidSource } from '@/services/mermaid/recoveryPresentation'; + +function getMermaidDiagnosticsBannerClass(hasError: boolean): string { + if (hasError) { + return 'border-amber-500/20 bg-amber-500/10 text-amber-500'; + } + + return 'border-sky-500/20 bg-sky-500/10 text-sky-500'; +} + +interface MermaidDiagnosticsBannerProps { + snapshot: MermaidDiagnosticsSnapshot; + className?: string; + actionLabel?: string; + onAction?: () => void; +} + +export function MermaidDiagnosticsBanner({ + snapshot, + className = '', + actionLabel, + onAction, +}: MermaidDiagnosticsBannerProps): React.ReactElement { + const message = + snapshot.error + || snapshot.statusDetail + || snapshot.diagnostics[0]?.message + || 'Mermaid diagnostics are available.'; + const showRecoveryHint = canRecoverMermaidSource({ + originalSource: snapshot.originalSource, + importState: snapshot.importState, + layoutMode: snapshot.layoutMode, + }); + + return ( +
+
+ + {snapshot.statusLabel ?? 'Mermaid diagnostics'} +
+
{message}
+ {showRecoveryHint ? ( +
+ Original Mermaid source is preserved. Open Mermaid code to continue editing safely. +
+ ) : null} + {actionLabel && onAction ? ( +
+ +
+ ) : null} +
+ ); +} diff --git a/src/components/MermaidSvgNode.tsx b/src/components/MermaidSvgNode.tsx new file mode 100644 index 00000000..2f7c0922 --- /dev/null +++ b/src/components/MermaidSvgNode.tsx @@ -0,0 +1,51 @@ +import React, { memo, useMemo } from 'react'; +import type { LegacyNodeProps } from '@/lib/reactflowCompat'; +import type { NodeData } from '@/lib/types'; +import { NodeChrome } from './NodeChrome'; + +function sanitizeMermaidSvgMarkup(svgMarkup: string | undefined): string | null { + if (typeof svgMarkup !== 'string' || svgMarkup.trim().length === 0) { + return null; + } + + // Mermaid owns the markup, but we still strip script tags as a hard safety floor. + return svgMarkup.replace(/[\s\S]*?<\/script>/gi, '').trim(); +} + +function MermaidSvgNode({ id, data, selected }: LegacyNodeProps): React.ReactElement { + const sanitizedSvg = useMemo(() => sanitizeMermaidSvgMarkup(data.mermaidSvg), [data.mermaidSvg]); + + return ( + +
+ {sanitizedSvg ? ( +
+ ) : ( +
+ Mermaid render unavailable +
+ )} +
+ + ); +} + +export default memo(MermaidSvgNode); diff --git a/src/components/SectionNode.tsx b/src/components/SectionNode.tsx index ef109855..7c41a3b3 100644 --- a/src/components/SectionNode.tsx +++ b/src/components/SectionNode.tsx @@ -4,13 +4,70 @@ import type { NodeData } from '@/lib/types'; import { getNodeParentId } from '@/lib/nodeParent'; import { Group, Lock, EyeOff } from 'lucide-react'; import { NamedIcon } from './IconMap'; +import { getNumericNodeDimension } from './nodeHelpers'; import { resolveSectionVisualStyle } from '../theme'; import { useInlineNodeTextEdit } from '@/hooks/useInlineNodeTextEdit'; import { InlineTextEditSurface } from './InlineTextEditSurface'; import { NodeChrome } from './NodeChrome'; import { useSelectionState } from '@/store/selectionHooks'; +import { readMermaidImportedNodeMetadataFromData } from '@/services/mermaid/importProvenance'; -function SectionNode({ id, data, selected }: LegacyNodeProps): React.ReactElement { +type SectionRenderVariant = 'default' | 'mermaid-import'; + +interface SectionRenderConfig { + variant: SectionRenderVariant; + bodyBorderRadius: string; + bodyInset: number; + titleTop: number; + titleLeft: number; + titlePadding: string; + titleBackgroundColor: string; + titleMaxWidth: string; + showLeadingIcon: boolean; + showImportedBadge: boolean; + showChildCount: boolean; +} + +function getSectionRenderConfig( + isImportedMermaidContainer: boolean, + borderColor: string +): SectionRenderConfig { + if (isImportedMermaidContainer) { + return { + variant: 'mermaid-import', + bodyBorderRadius: '12px', + bodyInset: 0, + titleTop: 8, + titleLeft: 10, + titlePadding: '0.15rem 0.45rem', + titleBackgroundColor: 'rgba(255,255,255,0.9)', + titleMaxWidth: 'calc(100% - 72px)', + showLeadingIcon: false, + showImportedBadge: true, + showChildCount: false, + }; + } + + return { + variant: 'default', + bodyBorderRadius: '16px', + bodyInset: 0, + titleTop: -36, + titleLeft: 0, + titlePadding: '0.375rem 0.625rem', + titleBackgroundColor: `${borderColor}22`, + titleMaxWidth: 'calc(100% - 8px)', + showLeadingIcon: true, + showImportedBadge: true, + showChildCount: true, + }; +} + +function SectionNode(props: LegacyNodeProps): React.ReactElement { + const { id, data, selected } = props; + const explicitNodeStyle = (props as { style?: React.CSSProperties }).style; + const explicitWidth = getNumericNodeDimension(explicitNodeStyle?.width); + const explicitHeight = getNumericNodeDimension(explicitNodeStyle?.height); const allNodes = useNodes(); const { hoveredSectionId } = useSelectionState(); const theme = resolveSectionVisualStyle(data.color, data.colorMode, data.customColor, 'blue'); @@ -24,39 +81,57 @@ function SectionNode({ id, data, selected }: LegacyNodeProps): React.R const isDropTarget = hoveredSectionId === id; const isLocked = data.sectionLocked === true; const isHidden = data.sectionHidden === true; + const isImportedMermaidContainer = + readMermaidImportedNodeMetadataFromData(data)?.role === 'container'; + const minWidth = isImportedMermaidContainer ? explicitWidth ?? 350 : 350; + const minHeight = isImportedMermaidContainer ? explicitHeight ?? 250 : 250; const borderColor = isDropTarget ? theme.title : theme.border; const bgColor = isDropTarget ? `color-mix(in srgb, ${theme.bg} 85%, white 15%)` : theme.bg; + const renderConfig = getSectionRenderConfig(isImportedMermaidContainer, borderColor); return (
- {/* Floating title โ€” sits ABOVE the section border, FigJam-style */}
- {iconName ? ( - - ) : ( - - )} + {renderConfig.showLeadingIcon ? ( + iconName ? ( + + ) : ( + + ) + ) : null} ): React.R onDraftChange={labelEdit.setDraft} onCommit={labelEdit.commit} onKeyDown={labelEdit.handleKeyDown} - className="font-semibold text-[13px] leading-tight tracking-tight whitespace-nowrap" + className={`font-semibold leading-tight tracking-tight whitespace-nowrap ${ + isImportedMermaidContainer ? 'text-[12px]' : 'text-[13px]' + }`} style={{ color: theme.title }} inputClassName="font-semibold" isSelected={Boolean(selected)} @@ -94,14 +171,26 @@ function SectionNode({ id, data, selected }: LegacyNodeProps): React.R {isHidden && }
)} + {renderConfig.showImportedBadge && isImportedMermaidContainer && ( + + Imported + + )}
- {/* Section body โ€” clean border, no internal header bar */}
): React.R }} /> - {/* Edge drag-handle strips for resize */}
- {/* Child count โ€” subtle bottom-right badge */} - {childCount > 0 && ( + {renderConfig.showChildCount && childCount > 0 && ( setViewSettings({ architectureStrictMode: checked })} /> +
+ +
+ {[ + { + mode: 'renderer_first', + label: t('settingsModal.canvas.mermaidImportModeRenderer', 'Fidelity-first'), + }, + { + mode: 'native_editable', + label: t('settingsModal.canvas.mermaidImportModeEditable', 'Editable-first'), + }, + ].map((option) => ( + + ))} +
+

+ {t( + 'settingsModal.canvas.mermaidImportModeDesc', + 'Fidelity-first keeps Mermaidโ€™s rendered geometry. Editable-first converts directly to native canvas nodes.' + )} +

+
@@ -204,7 +197,7 @@ export function StudioCodePanel({ placeholder={ mode === 'mermaid' ? t('commandBar.code.mermaidPlaceholder') - : t('commandBar.code.dslPlaceholder', { appName: APP_NAME }) + : t('commandBar.code.dslPlaceholder') } />
@@ -244,6 +237,10 @@ export function StudioCodePanel({
) : null} + {showMermaidDiagnosticsBanner ? ( + + ) : null} +
@@ -260,19 +257,7 @@ export function StudioCodePanel({ ) : null}
- {mode === 'openflow' ? ( - - - {t('commandBar.code.syntaxGuide')} - - ) : ( -
- )} +
({ - getProviderCatalogCount: vi.fn((provider: string) => (provider === 'aws' ? 2 : 0)), - loadProviderCatalog: vi.fn(async () => [ - { - id: 'aws-official-starter-v1:analytics-athena', - category: 'aws', +vi.mock('@/services/shapeLibrary/providerCatalog', async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + getProviderCatalogCount: vi.fn((provider: string) => (provider === 'aws' ? 2 : 0)), + loadProviderCatalog: vi.fn(async () => [ + { + id: 'aws-official-starter-v1:analytics-athena', + category: 'aws', + label: 'Analytics Athena', + description: 'AWS Analytics', + icon: 'Box', + color: 'amber', + nodeType: 'custom', + assetPresentation: 'icon', + providerShapeCategory: 'Analytics', + archIconPackId: 'aws-official-starter-v1', + archIconShapeId: 'analytics-athena', + }, + { + id: 'aws-official-starter-v1:compute-lambda', + category: 'aws', + label: 'Compute Lambda', + description: 'AWS Compute', + icon: 'Box', + color: 'amber', + nodeType: 'custom', + assetPresentation: 'icon', + providerShapeCategory: 'Compute', + archIconPackId: 'aws-official-starter-v1', + archIconShapeId: 'compute-lambda', + }, + ]), + loadProviderShapePreview: vi.fn(async () => ({ + packId: 'aws-official-starter-v1', + shapeId: 'analytics-athena', label: 'Analytics Athena', - description: 'AWS Analytics', - icon: 'Box', - color: 'amber', - nodeType: 'custom', - assetPresentation: 'icon', - providerShapeCategory: 'Analytics', - archIconPackId: 'aws-official-starter-v1', - archIconShapeId: 'analytics-athena', - }, - { - id: 'aws-official-starter-v1:compute-lambda', - category: 'aws', - label: 'Compute Lambda', - description: 'AWS Compute', - icon: 'Box', - color: 'amber', - nodeType: 'custom', - assetPresentation: 'icon', - providerShapeCategory: 'Compute', - archIconPackId: 'aws-official-starter-v1', - archIconShapeId: 'compute-lambda', - }, - ]), - loadProviderShapePreview: vi.fn(async () => ({ - packId: 'aws-official-starter-v1', - shapeId: 'analytics-athena', - label: 'Analytics Athena', - category: 'Analytics', - previewUrl: '/mock/athena.svg', - })), -})); + category: 'Analytics', + previewUrl: '/mock/athena.svg', + })), + }; +}); vi.mock('react-i18next', () => ({ useTranslation: () => ({ diff --git a/src/components/command-bar/applyCodeChanges.test.ts b/src/components/command-bar/applyCodeChanges.test.ts new file mode 100644 index 00000000..4378dbce --- /dev/null +++ b/src/components/command-bar/applyCodeChanges.test.ts @@ -0,0 +1,195 @@ +import { describe, expect, it, vi } from 'vitest'; +import { applyCodeChanges } from './applyCodeChanges'; +import { composeDiagramForDisplay } from '@/services/composeDiagramForDisplay'; +import { importMermaidToCanvas } from '@/services/mermaid/rendererFirstImport'; + +vi.mock('@/services/composeDiagramForDisplay', () => ({ + composeDiagramForDisplay: vi.fn(async (nodes, edges) => ({ nodes, edges })), +})); + +vi.mock('@/services/mermaid/rendererFirstImport', () => ({ + resolveEffectiveMermaidImportMode: vi.fn((importMode, diagramType) => + diagramType === 'flowchart' ? 'native_editable' : importMode + ), + importMermaidToCanvas: vi.fn(async ({ parsed, importMode }) => ({ + nodes: parsed.nodes, + edges: parsed.edges, + visualMode: 'editable_exact', + importMode, + })), +})); + +describe('applyCodeChanges', () => { + it('passes Mermaid import context into display composition for code apply', async () => { + const onApply = vi.fn(); + + const applied = await applyCodeChanges({ + mode: 'mermaid', + code: 'flowchart LR\nA-->B', + architectureStrictMode: false, + mermaidImportMode: 'native_editable', + onApply, + onClose: vi.fn(), + activeTabId: 'tab-1', + updateTab: vi.fn(), + setMermaidDiagnostics: vi.fn(), + clearMermaidDiagnostics: vi.fn(), + addToast: vi.fn(), + setError: vi.fn(), + setDiagnostics: vi.fn(), + setIsApplying: vi.fn(), + setLiveStatus: vi.fn(), + isLiveRequestStale: vi.fn(() => false), + options: { + closeOnSuccess: false, + source: 'manual', + }, + }); + + expect(applied).toBe(true); + expect(importMermaidToCanvas).toHaveBeenCalledWith( + expect.objectContaining({ + importMode: 'native_editable', + source: 'flowchart LR\nA-->B', + }) + ); + expect(onApply).toHaveBeenCalled(); + }); + + it('uses native editable Mermaid import by default for code apply', async () => { + const onApply = vi.fn(); + + const applied = await applyCodeChanges({ + mode: 'mermaid', + code: 'flowchart LR\nA-->B', + architectureStrictMode: false, + onApply, + onClose: vi.fn(), + activeTabId: 'tab-1', + updateTab: vi.fn(), + setMermaidDiagnostics: vi.fn(), + clearMermaidDiagnostics: vi.fn(), + addToast: vi.fn(), + setError: vi.fn(), + setDiagnostics: vi.fn(), + setIsApplying: vi.fn(), + setLiveStatus: vi.fn(), + isLiveRequestStale: vi.fn(() => false), + options: { + closeOnSuccess: false, + source: 'manual', + }, + }); + + expect(applied).toBe(true); + expect(importMermaidToCanvas).toHaveBeenCalledWith( + expect.objectContaining({ + importMode: 'native_editable', + }) + ); + expect(composeDiagramForDisplay).not.toHaveBeenCalled(); + expect(onApply).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + type: 'process', + }), + ]), + expect.arrayContaining([ + expect.objectContaining({ + source: 'A', + target: 'B', + }), + ]) + ); + }); + + it('blocks Mermaid apply when official validation rejects invalid syntax', async () => { + const setMermaidDiagnostics = vi.fn(); + const setError = vi.fn(); + const setDiagnostics = vi.fn(); + const addToast = vi.fn(); + + const applied = await applyCodeChanges({ + mode: 'mermaid', + code: 'flowchart TD\nA -->', + architectureStrictMode: false, + onApply: vi.fn(), + onClose: vi.fn(), + activeTabId: 'tab-1', + updateTab: vi.fn(), + setMermaidDiagnostics, + clearMermaidDiagnostics: vi.fn(), + addToast, + setError, + setDiagnostics, + setIsApplying: vi.fn(), + setLiveStatus: vi.fn(), + isLiveRequestStale: vi.fn(() => false), + options: { + closeOnSuccess: false, + source: 'manual', + }, + }); + + expect(applied).toBe(false); + expect(setError).toHaveBeenCalledWith(expect.stringContaining('Parse error')); + expect(setDiagnostics).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('Parse error'), + }), + ]) + ); + expect(setMermaidDiagnostics).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.stringContaining('Parse error'), + originalSource: 'flowchart TD\nA -->', + statusLabel: 'Unsupported Mermaid construct', + }) + ); + expect(addToast).toHaveBeenCalled(); + }); + + it('adds fallback guidance for unsupported Mermaid families', async () => { + const setMermaidDiagnostics = vi.fn(); + const setError = vi.fn(); + const setDiagnostics = vi.fn(); + const addToast = vi.fn(); + + const applied = await applyCodeChanges({ + mode: 'mermaid', + code: 'gitGraph\ncommit id: "A"', + architectureStrictMode: false, + onApply: vi.fn(), + onClose: vi.fn(), + activeTabId: 'tab-1', + updateTab: vi.fn(), + setMermaidDiagnostics, + clearMermaidDiagnostics: vi.fn(), + addToast, + setError, + setDiagnostics, + setIsApplying: vi.fn(), + setLiveStatus: vi.fn(), + isLiveRequestStale: vi.fn(() => false), + options: { + closeOnSuccess: false, + source: 'manual', + }, + }); + + expect(applied).toBe(false); + expect(setError).toHaveBeenCalledWith(expect.stringContaining('not editable yet')); + expect(setMermaidDiagnostics).toHaveBeenCalledWith( + expect.objectContaining({ + originalSource: 'gitGraph\ncommit id: "A"', + statusLabel: 'Unsupported Mermaid family', + }) + ); + expect( + addToast.mock.calls.some( + ([message]) => typeof message === 'string' && message.includes('not editable yet') + ) + ).toBe(true); + }); +}); diff --git a/src/components/command-bar/applyCodeChanges.ts b/src/components/command-bar/applyCodeChanges.ts index 835acdac..b875fac1 100644 --- a/src/components/command-bar/applyCodeChanges.ts +++ b/src/components/command-bar/applyCodeChanges.ts @@ -4,15 +4,28 @@ import { parseOpenFlowDSL } from '@/lib/openFlowDSLParser'; import type { MermaidDiagnosticsSnapshot } from '@/store/types'; import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType'; import { normalizeParseDiagnostics } from '@/services/mermaid/diagnosticFormatting'; +import { buildMermaidDiagnosticsSnapshot } from '@/services/mermaid/diagnosticsSnapshot'; +import { appendMermaidImportGuidance } from '@/services/mermaid/importStatePresentation'; +import { + getOfficialMermaidDiagnostics, + getOfficialMermaidErrorMessage, + isOfficialMermaidValidationBlocking, + validateMermaidWithOfficialParser, +} from '@/services/mermaid/officialMermaidValidation'; import { buildImportFidelityReport, mapErrorToIssue, + mapMermaidDiagnosticToIssue, mapParserDiagnosticToIssue, persistLatestImportReport, } from '@/services/importFidelity'; import { composeDiagramForDisplay } from '@/services/composeDiagramForDisplay'; -import type { FlowEdge, FlowNode } from '@/lib/types'; +import type { FlowEdge, FlowNode, MermaidImportMode } from '@/lib/types'; import { createImportReportOutcome, notifyOperationOutcome } from '@/services/operationFeedback'; +import { + importMermaidToCanvas, + resolveEffectiveMermaidImportMode, +} from '@/services/mermaid/rendererFirstImport'; const logger = createLogger({ scope: 'applyCodeChanges' }); @@ -26,6 +39,7 @@ interface ApplyCodeChangesParams { mode: 'mermaid' | 'openflow'; code: string; architectureStrictMode: boolean; + mermaidImportMode?: MermaidImportMode; onApply: (nodes: FlowNode[], edges: FlowEdge[]) => void; onClose: () => void; activeTabId: string; @@ -45,6 +59,7 @@ export async function applyCodeChanges({ mode, code, architectureStrictMode, + mermaidImportMode = 'renderer_first', onApply, onClose, activeTabId, @@ -60,6 +75,63 @@ export async function applyCodeChanges({ options, }: ApplyCodeChangesParams): Promise { const importStart = performance.now(); + const officialMermaidValidation = mode === 'mermaid' + ? await validateMermaidWithOfficialParser(code) + : null; + const officialDiagnostics = officialMermaidValidation + ? getOfficialMermaidDiagnostics(officialMermaidValidation) + : []; + + if (officialMermaidValidation && isOfficialMermaidValidationBlocking(officialMermaidValidation)) { + const rawErrorMessage = + getOfficialMermaidErrorMessage(officialMermaidValidation) + ?? 'Official Mermaid validation failed.'; + const errorMessage = appendMermaidImportGuidance({ + message: rawErrorMessage, + importState: officialMermaidValidation.detectedType ? 'unsupported_construct' : 'invalid_source', + diagramType: officialMermaidValidation.detectedType, + }); + + if (isLiveRequestStale(options.liveRequestId, options.source)) { + return false; + } + + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'code', + diagramType: officialMermaidValidation.detectedType, + importState: officialMermaidValidation.detectedType ? 'unsupported_construct' : 'invalid_source', + originalSource: code, + diagnostics: officialDiagnostics, + error: errorMessage, + }) + ); + + if (options.source === 'manual') { + const issues = officialMermaidValidation.diagnostics.map((diagnostic) => + mapMermaidDiagnosticToIssue(diagnostic) + ); + const report = buildImportFidelityReport({ + source: 'mermaid', + importState: officialMermaidValidation.detectedType ? 'unsupported_construct' : 'invalid_source', + originalSource: code, + nodeCount: 0, + edgeCount: 0, + elapsedMs: Math.round(performance.now() - importStart), + issues: issues.length > 0 ? issues : [mapErrorToIssue(errorMessage)], + }); + persistLatestImportReport(report); + notifyOperationOutcome(addToast, createImportReportOutcome(report, errorMessage)); + } + + setError(errorMessage); + setDiagnostics(officialDiagnostics); + if (options.source === 'live') { + setLiveStatus('error'); + } + return false; + } + const res = mode === 'mermaid' ? parseMermaidByType(code, { architectureStrictMode }) : parseOpenFlowDSL(code); @@ -71,35 +143,63 @@ export async function applyCodeChanges({ const parserDiagnostics = 'diagnostics' in res ? normalizeParseDiagnostics(res.diagnostics) : []; + const combinedDiagnostics = [...officialDiagnostics, ...parserDiagnostics]; if (mode === 'mermaid') { - setMermaidDiagnostics({ - source: 'code', + const userFacingError = appendMermaidImportGuidance({ + message: res.error, + importState: 'importState' in res ? res.importState : undefined, diagramType: 'diagramType' in res ? res.diagramType : undefined, - diagnostics: parserDiagnostics, - error: res.error, - updatedAt: Date.now(), }); + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'code', + diagramType: 'diagramType' in res ? res.diagramType : undefined, + importState: 'importState' in res ? res.importState : undefined, + originalSource: mode === 'mermaid' && 'originalSource' in res ? res.originalSource : code, + diagnostics: combinedDiagnostics, + error: userFacingError, + }) + ); + setError(userFacingError); + } else { + setError(res.error); } if (options.source === 'manual') { - const issues = parserDiagnostics.map((diagnostic) => mapParserDiagnosticToIssue(diagnostic)); + const issues = [ + ...officialDiagnostics.map((diagnostic) => mapParserDiagnosticToIssue(diagnostic)), + ...parserDiagnostics.map((diagnostic) => mapParserDiagnosticToIssue(diagnostic)), + ]; if (issues.length === 0) { issues.push(mapErrorToIssue(res.error)); } const report = buildImportFidelityReport({ source: mode === 'mermaid' ? 'mermaid' : 'openflowdsl', + importState: mode === 'mermaid' && 'importState' in res ? res.importState : undefined, + originalSource: mode === 'mermaid' ? ('originalSource' in res ? res.originalSource : code) : undefined, nodeCount: 0, edgeCount: 0, elapsedMs: Math.round(performance.now() - importStart), issues, }); persistLatestImportReport(report); - notifyOperationOutcome(addToast, createImportReportOutcome(report, res.error)); + notifyOperationOutcome( + addToast, + createImportReportOutcome( + report, + mode === 'mermaid' + ? appendMermaidImportGuidance({ + message: res.error, + importState: 'importState' in res ? res.importState : undefined, + diagramType: 'diagramType' in res ? res.diagramType : undefined, + }) + : res.error + ) + ); } - setError(res.error); if ('diagnostics' in res) { - setDiagnostics(normalizeParseDiagnostics(res.diagnostics)); + setDiagnostics(combinedDiagnostics); } else { - setDiagnostics([]); + setDiagnostics(officialDiagnostics); } if (options.source === 'live') { setLiveStatus('error'); @@ -117,47 +217,108 @@ export async function applyCodeChanges({ if (isLiveRequestStale(options.liveRequestId, options.source)) { return false; } - if (mode === 'mermaid') { - const parserDiagnostics = 'diagnostics' in res - ? normalizeParseDiagnostics(res.diagnostics) - : []; - if (parserDiagnostics.length > 0) { - setMermaidDiagnostics({ - source: 'code', - diagramType: 'diagramType' in res ? res.diagramType : undefined, - diagnostics: parserDiagnostics, - updatedAt: Date.now(), - }); - } else { - clearMermaidDiagnostics(); - } - } + const parserDiagnostics = mode === 'mermaid' && 'diagnostics' in res + ? normalizeParseDiagnostics(res.diagnostics) + : []; + const combinedDiagnostics = [...officialDiagnostics, ...parserDiagnostics]; + const effectiveMermaidImportMode = mode === 'mermaid' + ? resolveEffectiveMermaidImportMode( + mermaidImportMode, + 'diagramType' in res ? res.diagramType : undefined + ) + : mermaidImportMode; const direction = ('direction' in res && res.direction) ? res.direction : 'TB'; - const { nodes: layoutedNodes, edges: layoutedEdges } = await composeDiagramForDisplay(res.nodes, res.edges, { - direction, - algorithm: 'layered', - spacing: 'normal', - diagramType: mode === 'mermaid' && 'diagramType' in res ? res.diagramType : undefined, - }); + const canvasImport = + mode === 'mermaid' + ? await importMermaidToCanvas({ + parsed: res as typeof res & Parameters[0]['parsed'], + source: code, + importMode: effectiveMermaidImportMode, + layout: { + direction, + spacing: 'normal', + contentDensity: 'balanced', + }, + }) + : await composeDiagramForDisplay(res.nodes, res.edges, { + direction, + algorithm: 'layered', + spacing: 'normal', + }).then((layoutResult) => ({ + nodes: layoutResult.nodes, + edges: layoutResult.edges, + layoutMode: layoutResult.layoutMode, + layoutFallbackReason: layoutResult.layoutFallbackReason, + visualMode: 'editable_fallback' as const, + })); if (isLiveRequestStale(options.liveRequestId, options.source)) { return false; } - onApply(layoutedNodes, layoutedEdges); + if (mode === 'mermaid') { + const shouldSurfaceDiagnostics = + combinedDiagnostics.length > 0 + || canvasImport.visualMode === 'renderer_exact' + || canvasImport.visualMode !== 'editable_exact' + || canvasImport.layoutMode === 'mermaid_preserved_partial' + || canvasImport.layoutMode === 'mermaid_partial' + || canvasImport.layoutMode === 'elk_fallback'; + if (shouldSurfaceDiagnostics) { + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'code', + diagramType: 'diagramType' in res ? res.diagramType : undefined, + importState: 'importState' in res ? res.importState : undefined, + originalSource: mode === 'mermaid' && 'originalSource' in res ? res.originalSource : code, + diagnostics: combinedDiagnostics, + nodeCount: canvasImport.nodes.length, + edgeCount: canvasImport.edges.length, + layoutMode: canvasImport.layoutMode, + visualMode: canvasImport.visualMode, + layoutFallbackReason: canvasImport.layoutFallbackReason, + }) + ); + } else { + clearMermaidDiagnostics(); + } + } + + onApply( + mode === 'mermaid' + ? canvasImport.nodes.map((node) => ({ + ...node, + data: { + ...node.data, + _appliedFromMermaidImport: true, + }, + })) + : canvasImport.nodes, + canvasImport.edges + ); setError(null); setDiagnostics([]); if (mode === 'mermaid' && 'diagramType' in res && res.diagramType) { updateTab(activeTabId, { diagramType: res.diagramType }); } if (options.source === 'manual') { + const issues = mode === 'mermaid' && 'structuredDiagnostics' in res + ? [ + ...officialDiagnostics.map((diagnostic) => mapParserDiagnosticToIssue(diagnostic)), + ...(res.structuredDiagnostics ?? []).map((diagnostic) => mapMermaidDiagnosticToIssue(diagnostic)), + ] + : []; const report = buildImportFidelityReport({ source: mode === 'mermaid' ? 'mermaid' : 'openflowdsl', - nodeCount: layoutedNodes.length, - edgeCount: layoutedEdges.length, + importState: mode === 'mermaid' && 'importState' in res ? res.importState : undefined, + layoutMode: mode === 'mermaid' ? canvasImport.layoutMode : undefined, + layoutFallbackReason: mode === 'mermaid' ? canvasImport.layoutFallbackReason : undefined, + originalSource: mode === 'mermaid' ? ('originalSource' in res ? res.originalSource : code) : undefined, + nodeCount: canvasImport.nodes.length, + edgeCount: canvasImport.edges.length, elapsedMs: Math.round(performance.now() - importStart), - issues: [], + issues, }); persistLatestImportReport(report); notifyOperationOutcome(addToast, createImportReportOutcome(report)); @@ -178,6 +339,10 @@ export async function applyCodeChanges({ if (options.source === 'manual') { const report = buildImportFidelityReport({ source: mode === 'mermaid' ? 'mermaid' : 'openflowdsl', + importState: mode === 'mermaid' && 'importState' in res ? res.importState : undefined, + layoutMode: mode === 'mermaid' ? 'elk_fallback' : undefined, + layoutFallbackReason: mode === 'mermaid' ? 'Layout fallback applied after import.' : undefined, + originalSource: mode === 'mermaid' ? ('originalSource' in res ? res.originalSource : code) : undefined, nodeCount: res.nodes.length, edgeCount: res.edges.length, elapsedMs: Math.round(performance.now() - importStart), @@ -201,11 +366,12 @@ export async function applyCodeChanges({ const parserDiagnostics = 'diagnostics' in res ? normalizeParseDiagnostics(res.diagnostics) : []; - if (parserDiagnostics.length > 0) { + const combinedDiagnostics = [...officialDiagnostics, ...parserDiagnostics]; + if (combinedDiagnostics.length > 0) { setMermaidDiagnostics({ source: 'code', diagramType: 'diagramType' in res ? res.diagramType : undefined, - diagnostics: parserDiagnostics, + diagnostics: combinedDiagnostics, updatedAt: Date.now(), }); } else { @@ -222,10 +388,18 @@ export async function applyCodeChanges({ if (options.source === 'manual') { const report = buildImportFidelityReport({ source: mode === 'mermaid' ? 'mermaid' : 'openflowdsl', + importState: mode === 'mermaid' && 'importState' in res ? res.importState : undefined, + originalSource: mode === 'mermaid' ? ('originalSource' in res ? res.originalSource : code) : undefined, nodeCount: res.nodes.length, edgeCount: res.edges.length, elapsedMs: Math.round(performance.now() - importStart), - issues: [], + issues: + mode === 'mermaid' && 'structuredDiagnostics' in res + ? [ + ...officialDiagnostics.map((diagnostic) => mapParserDiagnosticToIssue(diagnostic)), + ...(res.structuredDiagnostics ?? []).map((diagnostic) => mapMermaidDiagnosticToIssue(diagnostic)), + ] + : [], }); persistLatestImportReport(report); notifyOperationOutcome(addToast, createImportReportOutcome(report)); diff --git a/src/components/command-bar/importViewModel.test.ts b/src/components/command-bar/importViewModel.test.ts index 07bb9432..3bbd729a 100644 --- a/src/components/command-bar/importViewModel.test.ts +++ b/src/components/command-bar/importViewModel.test.ts @@ -18,13 +18,16 @@ const t = ((key: string, fallback?: string) => fallback ?? key) as TFunction< describe('importViewModel', () => { it('returns translated category labels from the shared definitions', () => { - expect(getImportCategoryLabel(t, 'sql')).toBe('SQL'); - expect(getImportCategoryLabel(t, 'codebase')).toBe('Repo'); + expect(getImportCategoryLabel(t, 'infra')).toBe('Infra'); expect(getImportCategoryDefinition('infra').hasNative).toBe(true); - expect(getImportCategoryDefinition('openapi').hasNative).toBe(false); expect(IMPORT_CATEGORY_DEFINITIONS.some((definition) => definition.id === 'mermaid')).toBe( false ); + // sql, openapi, codebase are hidden behind feature flags (default off) + expect(IMPORT_CATEGORY_DEFINITIONS.some((definition) => definition.id === 'sql')).toBe(false); + expect(IMPORT_CATEGORY_DEFINITIONS.some((definition) => definition.id === 'codebase')).toBe( + false + ); }); it('builds placeholders and options for the import view controls', () => { @@ -38,7 +41,6 @@ describe('importViewModel', () => { 'terraform-state', 'kubernetes', 'docker-compose', - 'terraform-hcl', ]); expect(languageOptions.some((option) => option.value === 'typescript')).toBe(true); }); diff --git a/src/components/command-bar/importViewModel.ts b/src/components/command-bar/importViewModel.ts index cc26aaa8..076c346e 100644 --- a/src/components/command-bar/importViewModel.ts +++ b/src/components/command-bar/importViewModel.ts @@ -2,6 +2,7 @@ import type { TFunction } from 'i18next'; import type { SelectOption } from '@/components/ui/Select'; import { LANGUAGE_LABELS } from '@/hooks/ai-generation/codeToArchitecture'; import type { ImportCategory } from './importDetection'; +import { ROLLOUT_FLAGS, type RolloutFlagKey } from '@/config/rolloutFlags'; export interface ImportCategoryDefinition { id: ImportCategory; @@ -9,15 +10,17 @@ export interface ImportCategoryDefinition { labelKey: string; hasNative: boolean; hasAI: boolean; + featureFlag?: RolloutFlagKey; } -export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [ +const ALL_IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [ { id: 'sql', fallbackLabel: 'SQL', labelKey: 'commandBar.import.categories.sql', hasNative: true, hasAI: true, + featureFlag: 'importSql', }, { id: 'infra', @@ -32,6 +35,7 @@ export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [ labelKey: 'commandBar.import.categories.openapi', hasNative: false, hasAI: true, + featureFlag: 'importOpenApi', }, { id: 'code', @@ -46,9 +50,15 @@ export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [ labelKey: 'commandBar.import.categories.codebase', hasNative: true, hasAI: true, + featureFlag: 'importCodebase', }, ]; +export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = + ALL_IMPORT_CATEGORY_DEFINITIONS.filter( + (cat) => !cat.featureFlag || ROLLOUT_FLAGS[cat.featureFlag] + ); + export function createLanguageOptions(): SelectOption[] { return Object.entries(LANGUAGE_LABELS).map(([value, label]) => ({ value, @@ -76,7 +86,7 @@ export function getImportPlaceholders( } export function getInfraFormatOptions(t: TFunction<'translation', undefined>): SelectOption[] { - return [ + const options: SelectOption[] = [ { value: 'terraform-state', label: t('commandBar.import.infraFormats.terraformState', 'Terraform State (.tfstate)'), @@ -89,11 +99,16 @@ export function getInfraFormatOptions(t: TFunction<'translation', undefined>): S value: 'docker-compose', label: t('commandBar.import.infraFormats.dockerCompose', 'Docker Compose'), }, - { + ]; + + if (ROLLOUT_FLAGS.importInfraTerraformHcl) { + options.push({ value: 'terraform-hcl', label: t('commandBar.import.infraFormats.terraformHcl', 'Terraform HCL (AI)'), - }, - ]; + }); + } + + return options; } export function getImportCategoryLabel( diff --git a/src/components/command-bar/mermaidImportParser.ts b/src/components/command-bar/mermaidImportParser.ts index 8a5efe86..37a72f3a 100644 --- a/src/components/command-bar/mermaidImportParser.ts +++ b/src/components/command-bar/mermaidImportParser.ts @@ -1,5 +1,6 @@ import type { NativeParseResult } from './importNativeParsers'; import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType'; +import { summarizeMermaidImport } from '@/services/mermaid/importStatePresentation'; import { toOpenFlowDSL } from '@/services/openFlowDSLExporter'; export function parseMermaidToNative(input: string): NativeParseResult { @@ -18,6 +19,11 @@ export function parseMermaidToNative(input: string): NativeParseResult { dsl, nodeCount: result.nodes.length, edgeCount: result.edges.length, - summary: `Mermaid ${typeLabel}: ${result.nodes.length} node${result.nodes.length === 1 ? '' : 's'}, ${result.edges.length} edge${result.edges.length === 1 ? '' : 's'}`, + summary: summarizeMermaidImport({ + diagramType: typeLabel, + importState: result.importState, + nodeCount: result.nodes.length, + edgeCount: result.edges.length, + }), }; } diff --git a/src/components/command-bar/useCommandBarCommands.test.tsx b/src/components/command-bar/useCommandBarCommands.test.tsx index b2814d1e..cd7dcb58 100644 --- a/src/components/command-bar/useCommandBarCommands.test.tsx +++ b/src/components/command-bar/useCommandBarCommands.test.tsx @@ -36,7 +36,6 @@ describe('useCommandBarCommands', () => { 'search-nodes', 'layout', 'architecture-rules', - 'studio-openflow', 'studio-mermaid', 'toggle-grid', 'toggle-snap', @@ -50,16 +49,14 @@ describe('useCommandBarCommands', () => { expect(result.current.find((command) => command.id === 'templates')?.tier).toBe('core'); expect(result.current.find((command) => command.id === 'layout')?.tier).toBe('core'); expect(result.current.find((command) => command.id === 'assets')?.tier).toBe('advanced'); - expect(result.current.find((command) => command.id === 'studio-openflow')?.tier).toBe('advanced'); result.current.find((command) => command.id === 'studio-ai')?.action?.(); result.current.find((command) => command.id === 'architecture-rules')?.action?.(); - result.current.find((command) => command.id === 'studio-openflow')?.action?.(); result.current.find((command) => command.id === 'studio-mermaid')?.action?.(); expect(onOpenStudioAI).toHaveBeenCalledTimes(1); expect(onOpenArchitectureRules).toHaveBeenCalledTimes(1); - expect(onOpenStudioOpenFlow).toHaveBeenCalledTimes(1); + expect(onOpenStudioOpenFlow).toHaveBeenCalledTimes(0); expect(onOpenStudioMermaid).toHaveBeenCalledTimes(1); }); }); diff --git a/src/components/command-bar/useCommandBarCommands.tsx b/src/components/command-bar/useCommandBarCommands.tsx index 5e61b6e3..bf7e274e 100644 --- a/src/components/command-bar/useCommandBarCommands.tsx +++ b/src/components/command-bar/useCommandBarCommands.tsx @@ -3,7 +3,6 @@ import { ArrowRight, Code2, Compass, - FileCode, Import, Search, Settings, @@ -12,7 +11,7 @@ import { Workflow, } from 'lucide-react'; import { useFlowStore } from '@/store'; -import { APP_NAME, FLOWPILOT_NAME } from '@/lib/brand'; +import { FLOWPILOT_NAME } from '@/lib/brand'; import type { CommandItem, CommandBarProps } from './types'; import { AssetsIcon } from '../icons/AssetsIcon'; @@ -32,7 +31,7 @@ export function useCommandBarCommands({ onUndo, onRedo, onOpenStudioAI, - onOpenStudioOpenFlow, + onOpenStudioOpenFlow: _onOpenStudioOpenFlow, onOpenStudioMermaid, onOpenArchitectureRules, hasImport = false, @@ -136,15 +135,6 @@ export function useCommandBarCommands({ description: 'Open architecture guardrails and rule templates', action: onOpenArchitectureRules, }, - { - id: 'studio-openflow', - label: 'Edit Flow DSL', - icon: , - tier: 'advanced', - type: 'action', - description: `Open ${APP_NAME} DSL in Studio`, - action: onOpenStudioOpenFlow, - }, { id: 'studio-mermaid', label: 'Edit Mermaid Code', @@ -194,7 +184,6 @@ export function useCommandBarCommands({ onOpenArchitectureRules, onOpenStudioAI, onOpenStudioMermaid, - onOpenStudioOpenFlow, onRedo, onUndo, settings, diff --git a/src/components/custom-edge/SequenceMessageEdge.tsx b/src/components/custom-edge/SequenceMessageEdge.tsx index 80acaeda..69c72a15 100644 --- a/src/components/custom-edge/SequenceMessageEdge.tsx +++ b/src/components/custom-edge/SequenceMessageEdge.tsx @@ -7,7 +7,7 @@ import { SEQ_ACTOR_EXTRA_H, SEQ_MSG_OFFSET, SEQ_MSG_SPACING, -} from '@/components/custom-nodes/SequenceParticipantNode'; +} from '@/services/sequence/layoutConstants'; // Resolved at edge render time from source node data (passed via edge data). const SELF_LOOP_W = 56; diff --git a/src/components/custom-edge/pathUtils.test.ts b/src/components/custom-edge/pathUtils.test.ts index 98877ab9..374ee0c3 100644 --- a/src/components/custom-edge/pathUtils.test.ts +++ b/src/components/custom-edge/pathUtils.test.ts @@ -181,6 +181,283 @@ describe('buildEdgePath', () => { expect(Number.isFinite(result.labelY)).toBe(true); }); + it('uses node dimensions when building self-loop paths', () => { + const result = buildEdgePath( + { + id: 'edge-self', + source: 'a', + target: 'a', + sourceX: 100, + sourceY: 100, + targetX: 100, + targetY: 100, + sourcePosition: Position.Right, + targetPosition: Position.Right, + }, + [], + [{ ...NODE_A, width: 320, height: 120 }], + 'smoothstep' + ); + + expect(result.edgePath).toContain('C'); + expect(result.labelX).toBeGreaterThan(100 + 100); + }); + + it('nudges ELK label positions for dense sibling bundles', () => { + const allEdges = [ + { id: 'edge-1', source: 'a', target: 'b', sourceHandle: 'right', targetHandle: 'left' }, + { id: 'edge-2', source: 'a', target: 'c', sourceHandle: 'right', targetHandle: 'left' }, + { id: 'edge-3', source: 'a', target: 'd', sourceHandle: 'right', targetHandle: 'left' }, + ]; + + const first = buildEdgePath( + { + id: 'edge-1', + source: 'a', + target: 'b', + sourceX: 100, + sourceY: 100, + targetX: 260, + targetY: 40, + sourcePosition: Position.Right, + targetPosition: Position.Left, + sourceHandleId: 'right', + targetHandleId: 'left', + }, + allEdges, + THREE_TARGET_NODES, + 'smoothstep', + { + routingMode: 'elk', + elkPoints: [{ x: 180, y: 100 }, { x: 220, y: 40 }], + } + ); + const middle = buildEdgePath( + { + id: 'edge-2', + source: 'a', + target: 'c', + sourceX: 100, + sourceY: 100, + targetX: 260, + targetY: 100, + sourcePosition: Position.Right, + targetPosition: Position.Left, + sourceHandleId: 'right', + targetHandleId: 'left', + }, + allEdges, + THREE_TARGET_NODES, + 'smoothstep', + { + routingMode: 'elk', + elkPoints: [{ x: 180, y: 100 }, { x: 220, y: 100 }], + } + ); + + expect(first.labelY).not.toBe(middle.labelY); + }); + + it('uses Mermaid import-fixed geometry exactly when provided', () => { + const result = buildEdgePath( + { + id: 'edge-import', + source: 'a', + target: 'b', + sourceX: 0, + sourceY: 0, + targetX: 100, + targetY: 100, + sourcePosition: Position.Right, + targetPosition: Position.Left, + }, + [], + [NODE_A, NODE_B], + 'smoothstep', + { + routingMode: 'import-fixed', + importRoutePath: 'M 0 0 C 20 0, 80 100, 100 100', + importRoutePoints: [ + { x: 0, y: 0 }, + { x: 50, y: 50 }, + { x: 100, y: 100 }, + ], + } + ); + + expect(result.edgePath).toBe('M 0 0 C 20 0, 80 100, 100 100'); + expect(result.labelX).toBe(50); + expect(result.labelY).toBe(50); + }); + + it('uses orthogonal non-fanout routing for Mermaid preserved-endpoint edges', () => { + const allEdges = [ + { id: 'edge-1', source: 'a', target: 'b', sourceHandle: 'right', targetHandle: 'left' }, + { id: 'edge-2', source: 'a', target: 'c', sourceHandle: 'right', targetHandle: 'left' }, + { id: 'edge-3', source: 'a', target: 'd', sourceHandle: 'right', targetHandle: 'left' }, + ]; + + const result = buildEdgePath( + { + id: 'edge-1', + source: 'a', + target: 'b', + sourceX: 100, + sourceY: 100, + targetX: 260, + targetY: 40, + sourcePosition: Position.Right, + targetPosition: Position.Left, + sourceHandleId: 'right', + targetHandleId: 'left', + }, + allEdges, + THREE_TARGET_NODES, + 'bezier', + { + routingMode: 'auto', + mermaidPreservedEndpoints: true, + } + ); + + expect(getMovePoint(result.edgePath)).toBe('100,100'); + expect(result.edgePath).not.toContain('C'); + }); + + it('adds anchor clearance when Mermaid preserved-endpoint edges connect to imported containers', () => { + const result = buildEdgePath( + { + id: 'edge-container', + source: 'a', + target: 'b', + sourceX: 100, + sourceY: 100, + targetX: 260, + targetY: 100, + sourcePosition: Position.Right, + targetPosition: Position.Left, + sourceHandleId: 'right', + targetHandleId: 'left', + }, + [{ id: 'edge-container', source: 'a', target: 'b', sourceHandle: 'right', targetHandle: 'left' }], + [NODE_A, NODE_B], + 'smoothstep', + { + routingMode: 'auto', + mermaidPreservedEndpoints: true, + mermaidSourceContainer: true, + mermaidTargetContainer: true, + } + ); + + expect(getMovePoint(result.edgePath)).toBe('114,100'); + expect(getLastPoint(result.edgePath)).toBe('246,100'); + }); + + it('keeps decision-branch fanout for Mermaid preserved-endpoint edges', () => { + const decisionNode = { + id: 'decision', + position: { x: 100, y: 100 }, + width: 80, + height: 80, + data: { shape: 'diamond' }, + }; + const branchA = { id: 'branch-a', position: { x: 260, y: 40 }, width: 0, height: 0 }; + const branchB = { id: 'branch-b', position: { x: 260, y: 100 }, width: 0, height: 0 }; + const branchC = { id: 'branch-c', position: { x: 260, y: 160 }, width: 0, height: 0 }; + const branchEdges = [ + { id: 'branch-1', source: 'decision', target: 'branch-a', sourceHandle: 'right', targetHandle: 'left' }, + { id: 'branch-2', source: 'decision', target: 'branch-b', sourceHandle: 'right', targetHandle: 'left' }, + { id: 'branch-3', source: 'decision', target: 'branch-c', sourceHandle: 'right', targetHandle: 'left' }, + ]; + + const topBranch = buildEdgePath( + { + id: 'branch-1', + source: 'decision', + target: 'branch-a', + sourceX: 100, + sourceY: 100, + targetX: 260, + targetY: 40, + sourcePosition: Position.Right, + targetPosition: Position.Left, + sourceHandleId: 'right', + targetHandleId: 'left', + }, + branchEdges, + [decisionNode, branchA, branchB, branchC], + 'smoothstep', + { + routingMode: 'auto', + mermaidPreservedEndpoints: true, + } + ); + + const centerBranch = buildEdgePath( + { + id: 'branch-2', + source: 'decision', + target: 'branch-b', + sourceX: 100, + sourceY: 100, + targetX: 260, + targetY: 100, + sourcePosition: Position.Right, + targetPosition: Position.Left, + sourceHandleId: 'right', + targetHandleId: 'left', + }, + branchEdges, + [decisionNode, branchA, branchB, branchC], + 'smoothstep', + { + routingMode: 'auto', + mermaidPreservedEndpoints: true, + } + ); + + expect(getMovePoint(topBranch.edgePath)).toBe('112,100'); + expect(getMovePoint(centerBranch.edgePath)).toBe('112,100'); + expect(topBranch.edgePath).not.toBe(centerBranch.edgePath); + expect(topBranch.labelY).toBeLessThan(centerBranch.labelY); + }); + + it('adds shape-aware clearance for Mermaid preserved-endpoint decision anchors', () => { + const decisionNode = { + id: 'decision', + position: { x: 60, y: 60 }, + width: 80, + height: 80, + data: { shape: 'diamond' }, + }; + + const result = buildEdgePath( + { + id: 'edge-decision-preserved', + source: 'decision', + target: 'b', + sourceX: 100, + sourceY: 100, + targetX: 260, + targetY: 100, + sourcePosition: Position.Right, + targetPosition: Position.Left, + sourceHandleId: 'right', + targetHandleId: 'left', + }, + [{ id: 'edge-decision-preserved', source: 'decision', target: 'b', sourceHandle: 'right', targetHandle: 'left' }], + [decisionNode, NODE_B], + 'smoothstep', + { + routingMode: 'auto', + mermaidPreservedEndpoints: true, + } + ); + + expect(getMovePoint(result.edgePath)).toBe('112,100'); + }); + it('builds straight auto-routed paths when the straight renderer is used', () => { const result = buildEdgePath( { @@ -746,6 +1023,7 @@ describe('buildEdgePath', () => { expect(getMovePoint(first.edgePath)).toBe('100,100'); expect(first.labelX).toBe(160); - expect(first.labelY).toBe(70); + expect(first.labelY).toBeLessThan(70); + expect(first.labelY).toBeGreaterThan(60); }); }); diff --git a/src/components/custom-edge/pathUtils.ts b/src/components/custom-edge/pathUtils.ts index 17f420e3..c26b60e7 100644 --- a/src/components/custom-edge/pathUtils.ts +++ b/src/components/custom-edge/pathUtils.ts @@ -32,6 +32,30 @@ import type { const EDGE_ROUTING_FAST_PATH_THRESHOLD = 600; +function isDecisionLikeShape(shape: string | undefined): boolean { + return shape === 'diamond'; +} + +function shouldKeepMermaidBranchSpread( + preserveMermaidEndpoints: boolean, + shape: string | undefined +): boolean { + return preserveMermaidEndpoints && isDecisionLikeShape(shape); +} + +function getMermaidPreservedAnchorClearance( + preserveMermaidEndpoints: boolean, + isContainer: boolean | undefined, + shape: string | undefined +): number { + if (!preserveMermaidEndpoints) { + return 0; + } + + const containerClearance = isContainer ? 14 : 0; + return Math.max(containerClearance, getShapeAwareElkAnchorClearance(shape)); +} + export function buildEdgePath( params: EdgePathParams, allEdges: MinimalEdge[], @@ -40,18 +64,67 @@ export function buildEdgePath( options: EdgePathOptions = {} ): EdgePathResult { return measureDevPerformance('buildEdgePath', () => { + const sourceNode = getNodeById(allNodes, params.source); + const targetNode = getNodeById(allNodes, params.target); + const useMermaidPreservedEndpointRouting = options.mermaidPreservedEndpoints === true; + const effectiveForceOrthogonal = options.forceOrthogonal || useMermaidPreservedEndpointRouting; + const keepMermaidSourceBranchSpread = + shouldKeepMermaidBranchSpread(useMermaidPreservedEndpointRouting, sourceNode?.data?.shape); + const keepMermaidTargetBranchSpread = + shouldKeepMermaidBranchSpread(useMermaidPreservedEndpointRouting, targetNode?.data?.shape); + const sourceMermaidAnchorClearance = getMermaidPreservedAnchorClearance( + useMermaidPreservedEndpointRouting, + options.mermaidSourceContainer, + sourceNode?.data?.shape + ); + const targetMermaidAnchorClearance = getMermaidPreservedAnchorClearance( + useMermaidPreservedEndpointRouting, + options.mermaidTargetContainer, + targetNode?.data?.shape + ); const interactionLowDetailModeActive = isEdgeInteractionLowDetailModeActive(); const graphRoutingFastPathActive = interactionLowDetailModeActive || allEdges.length >= EDGE_ROUTING_FAST_PATH_THRESHOLD; + const pairOffset = graphRoutingFastPathActive || useMermaidPreservedEndpointRouting + ? 0 + : getParallelEdgeOffset(params.id, params.source, params.target, allEdges); + const sourceSiblingCount = graphRoutingFastPathActive || (useMermaidPreservedEndpointRouting && !keepMermaidSourceBranchSpread) + ? 0 + : getEndpointSiblingCount(allEdges, allNodes, { + nodeId: params.source, + handleId: params.sourceHandleId, + direction: 'source', + }); + const sourceFanoutOffset = graphRoutingFastPathActive || (useMermaidPreservedEndpointRouting && !keepMermaidSourceBranchSpread) + ? 0 + : getEndpointFanoutOffset(params.id, allEdges, allNodes, { + nodeId: params.source, + handleId: params.sourceHandleId, + direction: 'source', + }) * (useMermaidPreservedEndpointRouting ? 0.75 : 1); + const targetFanoutOffset = graphRoutingFastPathActive || (useMermaidPreservedEndpointRouting && !keepMermaidTargetBranchSpread) + ? 0 + : getEndpointFanoutOffset(params.id, allEdges, allNodes, { + nodeId: params.target, + handleId: params.targetHandleId, + direction: 'target', + }) * (useMermaidPreservedEndpointRouting ? 0.75 : 1); + const labelBundleOffset = pairOffset + (sourceFanoutOffset + targetFanoutOffset) / 2; if (params.source === params.target) { const loop = getSelfLoopPath( params.sourceX, params.sourceY, - 180, - 60, + sourceNode?.width ?? 180, + sourceNode?.height ?? 60, getLoopDirection(params.sourcePosition) ); - return { edgePath: loop.path, labelX: loop.labelX, labelY: loop.labelY }; + return withBundledLabelOffset( + loop.path, + loop.labelX, + loop.labelY, + params, + labelBundleOffset + ); } const shouldUseElkRoute = @@ -59,10 +132,48 @@ export function buildEdgePath( && options.elkPoints && options.elkPoints.length > 0; + const shouldUseImportedFixedRoute = + options.routingMode === 'import-fixed' + && ( + typeof options.importRoutePath === 'string' + || (options.importRoutePoints?.length ?? 0) > 0 + ); + + if (shouldUseImportedFixedRoute) { + const importRoutePoints = options.importRoutePoints ?? []; + const importRoutePath = options.importRoutePath; + + // Safety check: if the imported route's first point is more than 150px away from + // the actual ReactFlow handle position, the coordinates are in a different space + // (e.g. Mermaid SVG user-units vs ReactFlow canvas coordinates). In that case, + // skip the fixed route and fall back to smoothstep so edges are never disconnected. + const IMPORT_ROUTE_COORDINATE_MISMATCH_THRESHOLD = 150; + const firstPoint = importRoutePoints[0]; + const coordinatesMismatch = + firstPoint !== undefined && + Math.hypot( + firstPoint.x - params.sourceX, + firstPoint.y - params.sourceY + ) > IMPORT_ROUTE_COORDINATE_MISMATCH_THRESHOLD; + + if (!coordinatesMismatch) { + const labelPoint = importRoutePoints.length > 1 + ? getPathMidpoint(importRoutePoints) + : importRoutePoints[0] ?? { + x: (params.sourceX + params.targetX) / 2, + y: (params.sourceY + params.targetY) / 2, + }; + const pathStr = typeof importRoutePath === 'string' && importRoutePath.trim().length > 0 + ? importRoutePath + : buildRoundedPolylinePath(importRoutePoints, 12); + + return withBundledLabelOffset(pathStr, labelPoint.x, labelPoint.y, params, labelBundleOffset); + } + // Coordinate mismatch detected โ€” fall through to smoothstep routing below. + } + if (shouldUseElkRoute) { const points = options.elkPoints; - const sourceNode = getNodeById(allNodes, params.source); - const targetNode = getNodeById(allNodes, params.target); const adjustedSource = applyAnchorClearance( { x: params.sourceX, y: params.sourceY }, params.sourcePosition, @@ -81,42 +192,14 @@ export function buildEdgePath( const pathStr = buildRoundedPolylinePath(allPoints, 20); const { x: labelX, y: labelY } = getElkLabelPosition(adjustedSource.x, adjustedSource.y, points); - return { - edgePath: pathStr, - labelX, - labelY, - }; + return withBundledLabelOffset(pathStr, labelX, labelY, params, labelBundleOffset); } - const pairOffset = graphRoutingFastPathActive - ? 0 - : getParallelEdgeOffset(params.id, params.source, params.target, allEdges); - const isMindmapBranch = Boolean(options.mindmapBranchKind) && variant === 'bezier' && !options.forceOrthogonal; + const isMindmapBranch = Boolean(options.mindmapBranchKind) && variant === 'bezier' && !effectiveForceOrthogonal; const isMindmapRootBranch = options.mindmapBranchKind === 'root' && isMindmapBranch; - const sourceSiblingCount = graphRoutingFastPathActive - ? 0 - : getEndpointSiblingCount(allEdges, allNodes, { - nodeId: params.source, - handleId: params.sourceHandleId, - direction: 'source', - }); - const sourceFanoutOffset = graphRoutingFastPathActive - ? 0 - : getEndpointFanoutOffset(params.id, allEdges, allNodes, { - nodeId: params.source, - handleId: params.sourceHandleId, - direction: 'source', - }); - const targetFanoutOffset = graphRoutingFastPathActive - ? 0 - : getEndpointFanoutOffset(params.id, allEdges, allNodes, { - nodeId: params.target, - handleId: params.targetHandleId, - direction: 'target', - }); const shouldUseSharedSourceTrunk = !isMindmapBranch - && (variant === 'smoothstep' || variant === 'step' || options.forceOrthogonal) + && (variant === 'smoothstep' || variant === 'step' || effectiveForceOrthogonal) && sourceSiblingCount >= 3 && ( params.sourcePosition === Position.Left @@ -135,11 +218,20 @@ export function buildEdgePath( pairOffset + ((isMindmapBranch || shouldUseSharedSourceTrunk) ? 0 : sourceFanoutOffset) ); const targetOffset = getOffsetVector(params.targetPosition, pairOffset + targetFanoutOffset); - const labelBundleOffset = pairOffset + (sourceFanoutOffset + targetFanoutOffset) / 2; - const sourceX = params.sourceX + sourceOffset.x; - const sourceY = params.sourceY + sourceOffset.y; - const targetX = params.targetX + targetOffset.x; - const targetY = params.targetY + targetOffset.y; + const sourcePoint = applyAnchorClearance( + { x: params.sourceX + sourceOffset.x, y: params.sourceY + sourceOffset.y }, + params.sourcePosition, + sourceMermaidAnchorClearance + ); + const targetPoint = applyAnchorClearance( + { x: params.targetX + targetOffset.x, y: params.targetY + targetOffset.y }, + params.targetPosition, + targetMermaidAnchorClearance + ); + const sourceX = sourcePoint.x; + const sourceY = sourcePoint.y; + const targetX = targetPoint.x; + const targetY = targetPoint.y; const manualWaypoints = options.waypoints && options.waypoints.length > 0 ? options.waypoints @@ -159,7 +251,7 @@ export function buildEdgePath( ); } - if (variant === 'bezier' && !options.forceOrthogonal) { + if (variant === 'bezier' && !effectiveForceOrthogonal) { if (isMindmapBranch) { return withBundledLabelOffset( ...(() => { @@ -208,7 +300,7 @@ export function buildEdgePath( return withBundledLabelOffset(edgePath, labelX, labelY, params, labelBundleOffset); } - if (options.forceOrthogonal) { + if (effectiveForceOrthogonal) { const [edgePath, labelX, labelY] = getSmoothStepPath({ sourceX, sourceY, diff --git a/src/components/custom-edge/pathUtilsTypes.ts b/src/components/custom-edge/pathUtilsTypes.ts index 8b1e5a6d..8bf9c8ed 100644 --- a/src/components/custom-edge/pathUtilsTypes.ts +++ b/src/components/custom-edge/pathUtilsTypes.ts @@ -38,9 +38,14 @@ export type LoopDirection = 'right' | 'top' | 'left' | 'bottom'; export interface EdgePathOptions { forceOrthogonal?: boolean; + mermaidPreservedEndpoints?: boolean; + mermaidSourceContainer?: boolean; + mermaidTargetContainer?: boolean; elkPoints?: { x: number; y: number }[]; + importRoutePoints?: { x: number; y: number }[]; + importRoutePath?: string; mindmapBranchKind?: 'root' | 'branch'; - routingMode?: 'auto' | 'elk' | 'manual'; + routingMode?: 'auto' | 'elk' | 'manual' | 'import-fixed'; waypoints?: { x: number; y: number }[]; waypoint?: { x: number; diff --git a/src/components/custom-nodes/SequenceParticipantNode.tsx b/src/components/custom-nodes/SequenceParticipantNode.tsx index 9665a99a..c49243ac 100644 --- a/src/components/custom-nodes/SequenceParticipantNode.tsx +++ b/src/components/custom-nodes/SequenceParticipantNode.tsx @@ -7,19 +7,54 @@ import { InlineTextEditSurface } from '@/components/InlineTextEditSurface'; import { NodeChrome } from '@/components/NodeChrome'; import { getTransformDiagnosticsAttrs } from '@/components/transformDiagnostics'; import { resolveContainerVisualStyle } from '@/theme'; - -// These constants are used by SequenceMessageEdge to position arrows correctly. -export const SEQ_BOX_H = 48; -export const SEQ_ACTOR_EXTRA_H = 40; -export const SEQ_LIFELINE_H = 500; -export const SEQ_MSG_OFFSET = 20; // gap between box bottom and first message -export const SEQ_MSG_SPACING = 52; - -const SEQ_NODE_W = 140; +import { + SEQ_ACTOR_EXTRA_H, + SEQ_BOX_H, + SEQ_LIFELINE_H, + SEQ_NODE_W, + SEQ_MSG_OFFSET, + SEQ_MSG_SPACING, +} from '@/services/sequence/layoutConstants'; // Single invisible handle at top-center โ€” used by SequenceMessageEdge const TOP_HANDLE_ONLY = [{ id: 'top', position: Position.Top, side: 'top' as const }]; +function buildActivationRanges( + activations: Array<{ order: number; activate: boolean }> | undefined +): Array<{ startOrder: number; endOrder: number }> { + if (!activations || activations.length === 0) { + return []; + } + + const sortedActivations = [...activations].sort((left, right) => left.order - right.order); + const ranges: Array<{ startOrder: number; endOrder: number }> = []; + const openStack: number[] = []; + + sortedActivations.forEach((activation) => { + if (activation.activate) { + openStack.push(activation.order); + return; + } + + const startOrder = openStack.pop(); + if (typeof startOrder === 'number') { + ranges.push({ + startOrder, + endOrder: Math.max(startOrder + 1, activation.order), + }); + } + }); + + openStack.forEach((startOrder) => { + ranges.push({ + startOrder, + endOrder: startOrder + 1, + }); + }); + + return ranges.sort((left, right) => left.startOrder - right.startOrder); +} + function SequenceParticipantNode({ id, data, @@ -28,6 +63,7 @@ function SequenceParticipantNode({ const labelEdit = useInlineNodeTextEdit(id, 'label', data.label || ''); const isActor = data.seqParticipantKind === 'actor'; const totalH = (isActor ? SEQ_ACTOR_EXTRA_H : 0) + SEQ_BOX_H + SEQ_LIFELINE_H; + const activationRanges = buildActivationRanges(data.seqActivations); const visualStyle = resolveContainerVisualStyle( data.color, data.colorMode || 'subtle', @@ -96,7 +132,7 @@ function SequenceParticipantNode({ /> {/* Activation bars */} - {data.seqActivations && data.seqActivations.length > 0 && ( + {activationRanges.length > 0 && (
- {data.seqActivations.map((startOrder, i) => { - if (i % 2 !== 0) return null; - const endOrder = data.seqActivations![i + 1] ?? startOrder + 1; + {activationRanges.map(({ startOrder, endOrder }, i) => { const y = SEQ_MSG_OFFSET + startOrder * SEQ_MSG_SPACING; const h = (endOrder - startOrder) * SEQ_MSG_SPACING; return ( diff --git a/src/components/flow-canvas/flowCanvasTypes.test.ts b/src/components/flow-canvas/flowCanvasTypes.test.ts index f4591361..665999c4 100644 --- a/src/components/flow-canvas/flowCanvasTypes.test.ts +++ b/src/components/flow-canvas/flowCanvasTypes.test.ts @@ -16,9 +16,11 @@ describe('flowCanvasNodeTypes', () => { "er_entity", "image", "journey", + "mermaid_svg", "mindmap", "mobile", "process", + "section", "sequence_note", "sequence_participant", "start", diff --git a/src/components/flow-canvas/flowCanvasTypes.tsx b/src/components/flow-canvas/flowCanvasTypes.tsx index 3e0fbad8..c13b42b0 100644 --- a/src/components/flow-canvas/flowCanvasTypes.tsx +++ b/src/components/flow-canvas/flowCanvasTypes.tsx @@ -9,6 +9,7 @@ import { } from '@/components/CustomEdge'; import SequenceMessageEdge from '@/components/custom-edge/SequenceMessageEdge'; import ImageNode from '@/components/ImageNode'; +import MermaidSvgNode from '@/components/MermaidSvgNode'; import SwimlaneNode from '@/components/SwimlaneNode'; import TextNode from '@/components/TextNode'; import BrowserNode from '@/components/custom-nodes/BrowserNode'; @@ -20,6 +21,7 @@ import JourneyNode from '@/components/custom-nodes/JourneyNode'; import ArchitectureNode from '@/components/custom-nodes/ArchitectureNode'; import SequenceParticipantNode from '@/components/custom-nodes/SequenceParticipantNode'; import SequenceNoteNode from '@/components/custom-nodes/SequenceNoteNode'; +import SectionNode from '@/components/SectionNode'; export const flowCanvasNodeTypes: NodeTypes = { start: CustomNode, @@ -34,8 +36,10 @@ export const flowCanvasNodeTypes: NodeTypes = { architecture: ArchitectureNode, annotation: AnnotationNode, text: TextNode, + section: SectionNode, swimlane: SwimlaneNode, image: ImageNode, + mermaid_svg: MermaidSvgNode, browser: BrowserNode, mobile: MobileNode, sequence_participant: SequenceParticipantNode, diff --git a/src/components/flow-canvas/useFlowCanvasPaste.ts b/src/components/flow-canvas/useFlowCanvasPaste.ts index 8479c71b..f07ff4b5 100644 --- a/src/components/flow-canvas/useFlowCanvasPaste.ts +++ b/src/components/flow-canvas/useFlowCanvasPaste.ts @@ -1,162 +1,360 @@ import { useCallback } from 'react'; import { useFlowStore } from '@/store'; -import type { FlowEdge, FlowNode } from '@/lib/types'; + +import type { FlowEdge, FlowNode, MermaidImportMode } from '@/lib/types'; import type { MermaidDiagnosticsSnapshot } from '@/store/types'; -import { createPastedTextNode, isEditablePasteTarget, resolveLayoutDirection } from './pasteHelpers'; +import { + createPastedTextNode, + isEditablePasteTarget, + resolveLayoutDirection, +} from './pasteHelpers'; import { detectMermaidDiagramType } from '@/services/mermaid/detectDiagramType'; +import { extractMermaidDiagramHeader } from '@/services/mermaid/detectDiagramType'; import { normalizeParseDiagnostics } from '@/services/mermaid/diagnosticFormatting'; +import { buildMermaidDiagnosticsSnapshot } from '@/services/mermaid/diagnosticsSnapshot'; +import { + appendMermaidImportGuidance, + getMermaidImportToastMessage, +} from '@/services/mermaid/importStatePresentation'; +import { + getOfficialMermaidDiagnostics, + getOfficialMermaidErrorMessage, + isOfficialMermaidValidationBlocking, + validateMermaidWithOfficialParser, +} from '@/services/mermaid/officialMermaidValidation'; import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType'; -import { assignSmartHandles } from '@/services/smartEdgeRouting'; +import { enrichNodesWithIcons } from '@/lib/nodeEnricher'; +import { normalizeNodeIconData } from '@/lib/nodeIconState'; +import type { LayoutOptions } from '@/services/elk-layout/types'; +import { + importMermaidToCanvas, + resolveEffectiveMermaidImportMode, +} from '@/services/mermaid/rendererFirstImport'; + +const IMPORT_LABEL_COMPACT_THRESHOLD = 10; +const IMPORT_LABEL_VERBOSE_THRESHOLD = 20; +const IMPORT_LARGE_DIAGRAM_THRESHOLD = 36; + +function getAverageLabelLength(nodes: FlowNode[]): number { + if (nodes.length === 0) return 0; + const total = nodes.reduce((sum, node) => sum + String(node.data?.label ?? '').trim().length, 0); + return total / nodes.length; +} + +function resolveImportLayoutOptions( + nodes: FlowNode[], + diagramType?: string +): { spacing: NonNullable; contentDensity: NonNullable } { + const avg = getAverageLabelLength(nodes); + + let spacing: NonNullable; + if (diagramType === 'architecture') { + spacing = nodes.length >= 24 ? 'normal' : 'compact'; + } else if (avg <= IMPORT_LABEL_COMPACT_THRESHOLD) { + spacing = 'compact'; + } else if (avg <= IMPORT_LABEL_VERBOSE_THRESHOLD) { + spacing = nodes.length >= IMPORT_LARGE_DIAGRAM_THRESHOLD ? 'loose' : 'normal'; + } else { + spacing = 'loose'; + } + + const contentDensity: NonNullable = + avg <= IMPORT_LABEL_COMPACT_THRESHOLD ? 'compact' + : avg <= IMPORT_LABEL_VERBOSE_THRESHOLD ? 'balanced' + : 'verbose'; + + return { spacing, contentDensity }; +} type SetFlowNodes = (payload: FlowNode[] | ((nodes: FlowNode[]) => FlowNode[])) => void; type SetFlowEdges = (payload: FlowEdge[] | ((edges: FlowEdge[]) => FlowEdge[])) => void; -type AddToast = (message: string, type?: 'success' | 'error' | 'info' | 'warning', duration?: number) => void; +type AddToast = ( + message: string, + type?: 'success' | 'error' | 'info' | 'warning', + duration?: number +) => void; interface UseFlowCanvasPasteParams { - architectureStrictMode: boolean; - activeTabId: string; - fitView: (options?: { duration?: number; padding?: number }) => void; - updateTab: (tabId: string, updates: Partial<{ diagramType: string }>) => void; - recordHistory: () => void; - setNodes: SetFlowNodes; - setEdges: SetFlowEdges; - setSelectedNodeId: (id: string | null) => void; - setMermaidDiagnostics: (payload: MermaidDiagnosticsSnapshot | null) => void; - clearMermaidDiagnostics: () => void; - addToast: AddToast; - strictModePasteBlockedMessage: string; - pasteSelection: (center?: { x: number; y: number }) => void; - getLastInteractionFlowPosition: () => { x: number; y: number } | null; - getCanvasCenterFlowPosition: () => { x: number; y: number }; + architectureStrictMode: boolean; + mermaidImportMode: MermaidImportMode; + activeTabId: string; + fitView: (options?: { duration?: number; padding?: number }) => void; + updateTab: (tabId: string, updates: Partial<{ diagramType: string }>) => void; + recordHistory: () => void; + setNodes: SetFlowNodes; + setEdges: SetFlowEdges; + setSelectedNodeId: (id: string | null) => void; + setMermaidDiagnostics: (payload: MermaidDiagnosticsSnapshot | null) => void; + clearMermaidDiagnostics: () => void; + addToast: AddToast; + strictModePasteBlockedMessage: string; + pasteSelection: (center?: { x: number; y: number }) => void; + getLastInteractionFlowPosition: () => { x: number; y: number } | null; + getCanvasCenterFlowPosition: () => { x: number; y: number }; } export function useFlowCanvasPaste({ - architectureStrictMode, - activeTabId, - fitView, - updateTab, - recordHistory, - setNodes, - setEdges, - setSelectedNodeId, - setMermaidDiagnostics, - clearMermaidDiagnostics, - addToast, - strictModePasteBlockedMessage, - pasteSelection, - getLastInteractionFlowPosition, - getCanvasCenterFlowPosition, + architectureStrictMode, + mermaidImportMode, + activeTabId, + fitView, + updateTab, + recordHistory, + setNodes, + setEdges, + setSelectedNodeId, + setMermaidDiagnostics, + clearMermaidDiagnostics, + addToast, + strictModePasteBlockedMessage, + pasteSelection, + getLastInteractionFlowPosition, + getCanvasCenterFlowPosition, }: UseFlowCanvasPasteParams) { - const handleCanvasPaste = useCallback(async (event: React.ClipboardEvent): Promise => { - if (isEditablePasteTarget(event.target)) return; - const rawText = event.clipboardData.getData('text/plain'); - const pastedText = rawText.trim(); + const safelyEnrichImportedNodes = useCallback( + (nodes: FlowNode[], diagramType: MermaidDiagnosticsSnapshot['diagramType']): FlowNode[] => { + try { + return enrichNodesWithIcons(nodes, { + diagramType, + mode: 'mermaid-import', + }).map((node) => ({ + ...node, + data: normalizeNodeIconData(node.data), + })); + } catch { + addToast( + 'Imported diagram rendered without icon enrichment due to an enrichment error.', + 'warning' + ); + return nodes; + } + }, + [addToast] + ); + + const handleCanvasPaste = useCallback( + async (event: React.ClipboardEvent): Promise => { + if (isEditablePasteTarget(event.target)) return; + + const rawText = event.clipboardData.getData('text/plain'); + const pastedText = rawText.trim(); + + if (!pastedText) { + pasteSelection(getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition()); + return; + } + + event.preventDefault(); + + const mermaidHeader = extractMermaidDiagramHeader(pastedText); + const maybeMermaidType = mermaidHeader.diagramType ?? detectMermaidDiagramType(pastedText); + if (mermaidHeader.rawType) { + const officialMermaidValidation = await validateMermaidWithOfficialParser(pastedText); + const officialDiagnostics = getOfficialMermaidDiagnostics(officialMermaidValidation); + + if (isOfficialMermaidValidationBlocking(officialMermaidValidation)) { + const rawErrorMessage = + getOfficialMermaidErrorMessage(officialMermaidValidation) + ?? 'Official Mermaid validation failed.'; + const errorMessage = appendMermaidImportGuidance({ + message: rawErrorMessage, + importState: officialMermaidValidation.detectedType ? 'unsupported_construct' : 'invalid_source', + diagramType: officialMermaidValidation.detectedType ?? maybeMermaidType ?? undefined, + }); + + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'paste', + diagramType: officialMermaidValidation.detectedType ?? maybeMermaidType, + importState: officialMermaidValidation.detectedType ? 'unsupported_construct' : 'invalid_source', + originalSource: pastedText, + diagnostics: officialDiagnostics, + error: errorMessage, + }) + ); - if (!pastedText) { - pasteSelection(getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition()); - return; + addToast(errorMessage, 'error'); + return; } - event.preventDefault(); - - const maybeMermaidType = detectMermaidDiagramType(pastedText); - if (maybeMermaidType) { - const result = parseMermaidByType(pastedText, { architectureStrictMode }); - const diagnostics = normalizeParseDiagnostics(result.diagnostics); - - if (!result.error) { - if (diagnostics.length > 0) { - setMermaidDiagnostics({ - source: 'paste', - diagramType: result.diagramType, - diagnostics, - updatedAt: Date.now(), - }); - addToast(`Imported with ${diagnostics.length} diagnostic warning(s).`, 'warning'); - } else { - clearMermaidDiagnostics(); - } - - recordHistory(); - - if (result.nodes.length > 0) { - try { - const { getElkLayout } = await import('@/services/elkLayout'); - const layoutDirection = resolveLayoutDirection(result); - const { nodes: layoutedNodes, edges: layoutedEdges } = await getElkLayout(result.nodes, result.edges, { - direction: layoutDirection, - algorithm: 'layered', - spacing: 'normal', - }); - const smartEdges = assignSmartHandles(layoutedNodes, layoutedEdges); - setNodes(layoutedNodes); - setEdges(smartEdges); - } catch { - setNodes(result.nodes); - setEdges(result.edges); - } - } else { - setNodes(result.nodes); - setEdges(result.edges); - } - - if ('diagramType' in result && result.diagramType) { - updateTab(activeTabId, { diagramType: result.diagramType }); - } - - window.setTimeout(() => fitView({ duration: 600, padding: 0.2 }), 80); - return; - } + const result = parseMermaidByType(pastedText, { architectureStrictMode }); + const parserDiagnostics = normalizeParseDiagnostics(result.diagnostics); + const diagnostics = [...officialDiagnostics, ...parserDiagnostics]; + + if (!result.error) { + recordHistory(); + + if (result.nodes.length > 0) { + try { + const layoutDirection = resolveLayoutDirection(result); + const effectiveMermaidImportMode = resolveEffectiveMermaidImportMode( + mermaidImportMode, + result.diagramType + ); + const enrichedNodes = effectiveMermaidImportMode === 'native_editable' + ? safelyEnrichImportedNodes(result.nodes, result.diagramType) + : result.nodes; + const { spacing, contentDensity } = resolveImportLayoutOptions(enrichedNodes, result.diagramType); + const canvasImport = await importMermaidToCanvas({ + parsed: { ...result, nodes: enrichedNodes }, + source: pastedText, + importMode: effectiveMermaidImportMode, + layout: { + direction: layoutDirection, + spacing, + contentDensity, + }, + }); + + setNodes(canvasImport.nodes); + setEdges(canvasImport.edges); - setMermaidDiagnostics({ - source: 'paste', - diagramType: result.diagramType ?? maybeMermaidType, - diagnostics, - error: result.error, - updatedAt: Date.now(), - }); - - if (maybeMermaidType === 'architecture' && architectureStrictMode && result.error.includes('strict mode rejected')) { - addToast(strictModePasteBlockedMessage, 'error'); - return; + const shouldSurfaceDiagnostics = + diagnostics.length > 0 + || canvasImport.visualMode === 'renderer_exact' + || canvasImport.visualMode !== 'editable_exact' + || canvasImport.layoutMode === 'mermaid_preserved_partial' + || canvasImport.layoutMode === 'mermaid_partial' + || canvasImport.layoutMode === 'elk_fallback'; + + if (shouldSurfaceDiagnostics) { + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'paste', + diagramType: result.diagramType, + importState: result.importState, + originalSource: result.originalSource, + diagnostics, + nodeCount: canvasImport.nodes.length, + edgeCount: canvasImport.edges.length, + layoutMode: canvasImport.layoutMode, + visualMode: canvasImport.visualMode, + layoutFallbackReason: canvasImport.layoutFallbackReason, + }) + ); + } else { + clearMermaidDiagnostics(); + } + + const toastMessage = getMermaidImportToastMessage({ + importState: result.importState, + warningCount: + diagnostics.length + (canvasImport.visualMode === 'renderer_exact' ? 0 : 1), + }); + if (toastMessage && (diagnostics.length > 0 || canvasImport.visualMode !== 'renderer_exact')) { + addToast(toastMessage, 'warning'); + } + } catch { + const enrichedNodes = safelyEnrichImportedNodes(result.nodes, result.diagramType); + setNodes(enrichedNodes); + setEdges(result.edges); + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'paste', + diagramType: result.diagramType, + importState: result.importState, + originalSource: result.originalSource, + diagnostics, + nodeCount: result.nodes.length, + edgeCount: result.edges.length, + layoutMode: 'elk_fallback', + visualMode: 'editable_fallback', + layoutFallbackReason: 'Import layout orchestration failed after parsing', + }) + ); } + } else { + setNodes(result.nodes); + setEdges(result.edges); + if (diagnostics.length > 0) { + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'paste', + diagramType: result.diagramType, + importState: result.importState, + originalSource: result.originalSource, + diagnostics, + nodeCount: result.nodes.length, + edgeCount: result.edges.length, + }) + ); + } else { + clearMermaidDiagnostics(); + } + } + + if ('diagramType' in result && result.diagramType) { + updateTab(activeTabId, { diagramType: result.diagramType }); + } - addToast(result.error, 'error'); - return; + window.setTimeout(() => fitView({ duration: 600, padding: 0.2 }), 80); + return; } - const pasteFlowPosition = - getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition(); - - recordHistory(); - const { activeLayerId } = useFlowStore.getState(); - const newTextNode = createPastedTextNode(pastedText, pasteFlowPosition, activeLayerId); - - setNodes((existingNodes) => [ - ...existingNodes.map((node) => ({ ...node, selected: false })), - { ...newTextNode, selected: true }, - ]); - setSelectedNodeId(newTextNode.id); - }, [ - activeTabId, - addToast, - architectureStrictMode, - clearMermaidDiagnostics, - fitView, - getCanvasCenterFlowPosition, - pasteSelection, - getLastInteractionFlowPosition, - recordHistory, - setEdges, - setMermaidDiagnostics, - setNodes, - setSelectedNodeId, - strictModePasteBlockedMessage, - updateTab, - ]); - - return { - handleCanvasPaste, - }; + const errorMessage = appendMermaidImportGuidance({ + message: result.error, + importState: result.importState, + diagramType: result.diagramType ?? maybeMermaidType ?? undefined, + }); + setMermaidDiagnostics( + buildMermaidDiagnosticsSnapshot({ + source: 'paste', + diagramType: result.diagramType ?? maybeMermaidType, + importState: result.importState, + originalSource: result.originalSource, + diagnostics, + error: errorMessage, + }) + ); + + if ( + maybeMermaidType === 'architecture' && + architectureStrictMode && + result.error.includes('strict mode rejected') + ) { + addToast(strictModePasteBlockedMessage, 'error'); + return; + } + + addToast(errorMessage, 'error'); + return; + } + + const pasteFlowPosition = getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition(); + + recordHistory(); + const { activeLayerId } = useFlowStore.getState(); + const newTextNode = createPastedTextNode(pastedText, pasteFlowPosition, activeLayerId); + + setNodes((existingNodes) => [ + ...existingNodes.map((node) => ({ ...node, selected: false })), + { ...newTextNode, selected: true }, + ]); + setSelectedNodeId(newTextNode.id); + }, + [ + activeTabId, + addToast, + architectureStrictMode, + clearMermaidDiagnostics, + fitView, + getCanvasCenterFlowPosition, + pasteSelection, + getLastInteractionFlowPosition, + recordHistory, + mermaidImportMode, + setEdges, + setMermaidDiagnostics, + setNodes, + setSelectedNodeId, + safelyEnrichImportedNodes, + strictModePasteBlockedMessage, + updateTab, + ] + ); + + return { + handleCanvasPaste, + }; } diff --git a/src/components/flow-editor/useFlowEditorController.ts b/src/components/flow-editor/useFlowEditorController.ts index 4d94113c..7acee867 100644 --- a/src/components/flow-editor/useFlowEditorController.ts +++ b/src/components/flow-editor/useFlowEditorController.ts @@ -372,6 +372,7 @@ export function useFlowEditorController({ return { shouldRenderPanels, handleCanvasEntityIntent, + openStudioCode, panels, chrome, }; diff --git a/src/components/nodeHelpers.ts b/src/components/nodeHelpers.ts index aa4e9c4d..f6b43615 100644 --- a/src/components/nodeHelpers.ts +++ b/src/components/nodeHelpers.ts @@ -72,6 +72,20 @@ export function getIconAssetNodeMinSize(hasLabel: boolean): { } export function resolveNodeSize(node: FlowNode): { width: number; height: number } { + if (node.type === 'mermaid_svg') { + const styleWidth = typeof node.style?.width === 'number' ? node.style.width : undefined; + const styleHeight = typeof node.style?.height === 'number' ? node.style.height : undefined; + const dataWidth = typeof node.data?.width === 'number' ? node.data.width : undefined; + const dataHeight = typeof node.data?.height === 'number' ? node.data.height : undefined; + const nodeWidth = typeof node.width === 'number' ? node.width : undefined; + const nodeHeight = typeof node.height === 'number' ? node.height : undefined; + + return { + width: dataWidth ?? styleWidth ?? nodeWidth ?? 640, + height: dataHeight ?? styleHeight ?? nodeHeight ?? 480, + }; + } + const minSize = node.data?.assetPresentation === 'icon' ? getIconAssetNodeMinSize(Boolean(node.data?.label?.trim())) : getMinNodeSize(node.data?.shape); @@ -93,6 +107,10 @@ export function toCssSize(value: number | string | undefined): string | undefine return typeof value === 'number' ? `${value}px` : value; } +export function getNumericNodeDimension(value: unknown): number | undefined { + return typeof value === 'number' && Number.isFinite(value) ? value : undefined; +} + export function getNodeBorderRadius( isComplexShape: boolean, activeShape: NodeShape, diff --git a/src/components/properties/BulkNodeProperties.tsx b/src/components/properties/BulkNodeProperties.tsx index 1b9945ab..34cc961e 100644 --- a/src/components/properties/BulkNodeProperties.tsx +++ b/src/components/properties/BulkNodeProperties.tsx @@ -42,6 +42,11 @@ import { type BulkSectionId, type BulkNodePropertiesFormState, } from './bulkNodePropertiesModel'; +import { + createBuiltInIconData, + createProviderIconData, + createUploadedIconData, +} from '@/lib/nodeIconState'; interface BulkNodePropertiesProps { selectedNodes: Node[]; @@ -138,41 +143,49 @@ export function BulkNodeProperties({ } function handleBuiltInIconChange(nextIcon: string): void { + const updates = createBuiltInIconData(nextIcon); setForm((current) => ({ ...current, iconMode: 'built-in', - icon: nextIcon, - customIconUrl: undefined, - assetProvider: undefined, - assetCategory: undefined, - archIconPackId: undefined, - archIconShapeId: undefined, + icon: updates.icon ?? '', + customIconUrl: updates.customIconUrl, + assetProvider: updates.assetProvider as BulkNodePropertiesFormState['assetProvider'], + assetCategory: updates.assetCategory, + archIconPackId: updates.archIconPackId, + archIconShapeId: updates.archIconShapeId, })); } function handleProviderIconChange(selection: ProviderIconSelection): void { + const updates = createProviderIconData({ + packId: selection.packId, + shapeId: selection.shapeId, + provider: selection.provider, + category: selection.category, + }); setForm((current) => ({ ...current, iconMode: 'provider', - icon: '', - customIconUrl: undefined, - assetProvider: selection.provider, - assetCategory: selection.category, - archIconPackId: selection.packId, - archIconShapeId: selection.shapeId, + icon: updates.icon ?? '', + customIconUrl: updates.customIconUrl, + assetProvider: updates.assetProvider as BulkNodePropertiesFormState['assetProvider'], + assetCategory: updates.assetCategory, + archIconPackId: updates.archIconPackId, + archIconShapeId: updates.archIconShapeId, })); } function handleCustomIconChange(url?: string): void { + const updates = createUploadedIconData(url); setForm((current) => ({ ...current, iconMode: url ? 'upload' : '', - icon: '', - customIconUrl: url, - assetProvider: undefined, - assetCategory: undefined, - archIconPackId: undefined, - archIconShapeId: undefined, + icon: updates.icon ?? '', + customIconUrl: updates.customIconUrl, + assetProvider: updates.assetProvider as BulkNodePropertiesFormState['assetProvider'], + assetCategory: updates.assetCategory, + archIconPackId: updates.archIconPackId, + archIconShapeId: updates.archIconShapeId, })); } diff --git a/src/components/properties/IconPicker.tsx b/src/components/properties/IconPicker.tsx index ef8f8802..36c52e09 100644 --- a/src/components/properties/IconPicker.tsx +++ b/src/components/properties/IconPicker.tsx @@ -8,6 +8,7 @@ import { loadProviderShapePreview, } from '@/services/shapeLibrary/providerCatalog'; import { useAssetCatalog } from '@/hooks/useAssetCatalog'; +import { inferAssetProviderFromPackId } from '@/lib/nodeIconState'; import { ICON_NAMES, ICON_PICKER_PRIORITY_NAMES, NamedIcon } from '../IconMap'; import { Tooltip } from '../Tooltip'; import { Select } from '../ui/Select'; @@ -72,16 +73,6 @@ function getInitialSource( return 'built-in'; } -function inferProviderFromPackId(packId: string | undefined): DomainLibraryCategory | undefined { - if (!packId) { - return undefined; - } - - const normalizedPackId = packId.toLowerCase(); - const match = PROVIDER_OPTIONS.find((option) => normalizedPackId.includes(option.value)); - return match?.value as DomainLibraryCategory | undefined; -} - function getProviderLabel(provider: DomainLibraryCategory): string { return getAssetCategoryDisplayName(provider); } @@ -103,7 +94,7 @@ export const IconPicker: React.FC = ({ ); const [provider, setProvider] = useState( selectedProvider - ?? inferProviderFromPackId(selectedProviderPackId) + ?? inferAssetProviderFromPackId(selectedProviderPackId) ?? (PROVIDER_OPTIONS[0]?.value as DomainLibraryCategory) ?? 'aws' ); @@ -117,7 +108,7 @@ export const IconPicker: React.FC = ({ setProvider(selectedProvider); return; } - const inferredProvider = inferProviderFromPackId(selectedProviderPackId); + const inferredProvider = inferAssetProviderFromPackId(selectedProviderPackId); if (inferredProvider) { setProvider(inferredProvider); } diff --git a/src/components/properties/NodeProperties.test.tsx b/src/components/properties/NodeProperties.test.tsx index 5cd9121b..81d5acda 100644 --- a/src/components/properties/NodeProperties.test.tsx +++ b/src/components/properties/NodeProperties.test.tsx @@ -58,4 +58,26 @@ describe('NodeProperties', () => { expect(screen.getByText('Secondary Style')).toBeTruthy(); expect(screen.queryByRole('button', { name: 'Text Style' })).toBeNull(); }); + + it('uses the shared icon picker for icon-backed asset nodes', () => { + render( + + ); + + expect(screen.getByRole('button', { name: 'Icon' })).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByText('icon-picker')).toBeTruthy(); + }); }); diff --git a/src/components/properties/NodeProperties.tsx b/src/components/properties/NodeProperties.tsx index b567ce27..03ef3a74 100644 --- a/src/components/properties/NodeProperties.tsx +++ b/src/components/properties/NodeProperties.tsx @@ -9,20 +9,19 @@ import { IconPicker, type ProviderIconSelection } from './IconPicker'; import { ImageUpload } from './ImageUpload'; import { CollapsibleSection } from '../ui/CollapsibleSection'; import { useMarkdownEditor } from '@/hooks/useMarkdownEditor'; -import { useAssetCatalog } from '@/hooks/useAssetCatalog'; import { NodeActionButtons } from './NodeActionButtons'; import { NodeContentSection } from './NodeContentSection'; import { NodeImageSettingsSection } from './NodeImageSettingsSection'; import { NodeWireframeVariantSection } from './NodeWireframeVariantSection'; import { InspectorSectionDivider } from './InspectorPrimitives'; -import { Tooltip } from '../Tooltip'; -import { Select } from '../ui/Select'; import type { DomainLibraryCategory } from '@/services/domainLibrary'; -import { getAssetCategoryDisplayName, getAssetCategoryNoun } from '@/services/assetPresentation'; -import { loadProviderShapePreview } from '@/services/shapeLibrary/providerCatalog'; -import { NamedIcon } from '../IconMap'; -import { createPropertyInputKeyDownHandler } from './propertyInputBehavior'; -import { IconSearchField, IconTileScrollGrid } from './IconTilePickerPrimitives'; +import { getAssetCategoryDisplayName } from '@/services/assetPresentation'; +import { + createBuiltInIconData, + createProviderIconData, + createUploadedIconData, + normalizeNodeIconData, +} from '@/lib/nodeIconState'; import { getNodeParentId } from '@/lib/nodeParent'; import { buildSectionActions } from './sectionActionBuilder'; @@ -52,8 +51,13 @@ export const NodeProperties: React.FC = ({ const isSection = selectedNode.type === 'section'; const isGroup = selectedNode.type === 'group'; const isWireframeApp = selectedNode.type === 'browser' || selectedNode.type === 'mobile'; - const isIconAssetNode = selectedNode.data?.assetPresentation === 'icon'; - const assetProvider = (selectedNode.data?.assetProvider || '') as DomainLibraryCategory; + const normalizedIconData = normalizeNodeIconData(selectedNode.data); + const isIconAssetNode = normalizedIconData?.assetPresentation === 'icon'; + const assetProvider = normalizedIconData?.assetProvider as DomainLibraryCategory | undefined; + const assetCategory = + typeof normalizedIconData?.assetCategory === 'string' + ? normalizedIconData.assetCategory + : undefined; const supportsAdvancedColorTheme = ['process', 'start', 'end', 'decision', 'custom'].includes( selectedNode.type || '' ); @@ -73,34 +77,10 @@ export const NodeProperties: React.FC = ({ onChange, }); - const { - items: assetItems, - filteredItems: filteredAssetItems, - previewUrls: assetPreviewUrls, - query: assetQuery, - setQuery: setAssetQuery, - category: assetFilterCategory, - setCategory: setAssetFilterCategory, - } = useAssetCatalog({ - provider: assetProvider, - }); - const assetCategories = React.useMemo( - () => - Array.from( - new Set( - assetItems - .map((item) => item.providerShapeCategory) - .filter((value): value is string => Boolean(value)) - ) - ).sort((left, right) => left.localeCompare(right)), - [assetItems] - ); - const handlePropertyInputKeyDown = createPropertyInputKeyDownHandler({ blurOnEnter: true }); - function getDefaultSection(): string { if (isImage) return 'image'; if (isWireframeApp) return 'variant'; - if (isIconAssetNode) return 'asset'; + if (isIconAssetNode) return 'icon'; if (isSection) return 'content'; if (isText || isAnnotation) return 'content'; return 'content'; @@ -155,36 +135,23 @@ export const NodeProperties: React.FC = ({ } function handleBuiltInIconChange(icon: string): void { - onChange(selectedNode.id, { - icon, - customIconUrl: undefined, - archIconPackId: undefined, - archIconShapeId: undefined, - assetProvider: undefined, - assetCategory: undefined, - }); + onChange(selectedNode.id, createBuiltInIconData(icon)); } function handleProviderIconChange(selection: ProviderIconSelection): void { - onChange(selectedNode.id, { - icon: undefined, - customIconUrl: undefined, - archIconPackId: selection.packId, - archIconShapeId: selection.shapeId, - assetProvider: selection.provider, - assetCategory: selection.category, - }); + onChange( + selectedNode.id, + createProviderIconData({ + packId: selection.packId, + shapeId: selection.shapeId, + provider: selection.provider, + category: selection.category, + }) + ); } function handleCustomIconChange(url?: string): void { - onChange(selectedNode.id, { - icon: undefined, - customIconUrl: url, - archIconPackId: undefined, - archIconShapeId: undefined, - assetProvider: undefined, - assetCategory: undefined, - }); + onChange(selectedNode.id, createUploadedIconData(url)); } return ( @@ -231,84 +198,6 @@ export const NodeProperties: React.FC = ({ /> )} - {isIconAssetNode && ( - } - isOpen={activeSection === 'asset' || activeSection === 'shape'} - onToggle={() => toggleSection('asset')} - > -
- setAssetQuery(event.target.value)} - onKeyDown={handlePropertyInputKeyDown} - placeholder={`Search ${getAssetCategoryDisplayName(assetProvider || 'icons').toLowerCase()} ${getAssetCategoryNoun(assetProvider || 'icons')}`} - /> - {assetCategories.length > 1 ? ( -
๐Ÿง‘โ€๐Ÿ’ป Code โ†’ Diagram
SQL ยท Terraform ยท K8s
OpenAPI ยท Source code
โœจ Mermaid โ†’ Icons
Paste Mermaid ยท 1,600+ icons
auto-assigned ยท beautiful
๐Ÿค– AI Generation
9 providers ยท BYOK
Direct-to-canvas output
`{}` Diagram as Code
Bidirectional live sync
Git-friendly DSL
๐Ÿงฉ Asset Libraries
Developer ยท AWS ยท Azure
GCP ยท CNCF ยท Icons