diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 17d0a63..15ade00 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -74,9 +74,17 @@ Key area: ## State Management -The app uses a single Zustand store assembled in `src/store.ts`. +The app uses a single public Zustand store exported from `src/store.ts`. -The store is composed from action factories and supported by selective hook files in `src/store/`. +The runtime store is now bootstrapped through: + +- `src/store/createFlowStore.ts` +- `src/store/createFlowStoreState.ts` +- `src/store/createFlowStorePersistOptions.ts` + +This keeps the public entry stable while moving composition, persistence, and hydration concerns behind explicit seams. + +The store is still monolithic at runtime, but it is now partitioned more clearly through slice-typed hooks, selectors, and internal slice factories in `src/store/`. Current store-facing hook files include: @@ -91,6 +99,10 @@ Supporting files: - `defaults.ts` - `types.ts` +- `selectors.ts` +- `slices/createCanvasEditorSlice.ts` +- `slices/createExperienceSlice.ts` +- `slices/createWorkspaceSlice.ts` - `persistence.ts` - `aiSettings.ts` @@ -104,6 +116,7 @@ Persistence is coordinated through: - `src/store/persistence.ts` - `src/services/storage/flowPersistStorage.ts` +- `src/services/storage/storageRuntime.ts` - `src/services/storage/indexedDbStateStorage.ts` Current behavior at a high level: @@ -113,6 +126,10 @@ Current behavior at a high level: - localStorage remains part of the compatibility and fallback story - persisted nodes/edges are sanitized before storage - ephemeral UI fields are excluded from persisted state +- browser storage detection and IndexedDB schema readiness are now funneled through a shared storage runtime helper instead of each storage surface bootstrapping itself independently +- IndexedDB store and index definitions are now declared in one schema manifest in `src/services/storage/indexedDbSchema.ts` +- schema migration markers now live in a dedicated IndexedDB schema metadata store instead of sharing the persisted Zustand state store +- local-first chat persistence now uses document-scoped IndexedDB indexes instead of full chat-message store scans Important constraint: @@ -219,6 +236,12 @@ Examples include: These plugins and registrations allow the app to support multiple structured diagram behaviors without collapsing all logic into the base canvas layer. +Built-in diagram capabilities are now bootstrapped through a shared runtime initialization path instead of scattered one-off registration calls: + +- `src/diagram-types/bootstrap.ts` +- `src/diagram-types/builtInPlugins.ts` +- `src/diagram-types/builtInPropertyPanels.ts` + --- ## Docs Surfaces @@ -244,6 +267,7 @@ Collaboration currently lives under: Current implementation notes: +- collaboration runtime construction now flows through `src/services/collaboration/bootstrap.ts` - realtime transport is built around peer-oriented collaboration - the current stack includes WebRTC-style transport concerns and signaling configuration - fallback behavior exists for unsupported environments diff --git a/README.md b/README.md index 19e9acd..64ca18f 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ - + @@ -81,7 +81,7 @@ Every diagramming tool makes a compromise. OpenFlowKit doesn't. | ----------------------- | ----------------------------------------------------------------------------- | | **Excalidraw / tldraw** | Freeform whiteboards — no structured diagram types, no DSL, no code imports | | **Mermaid.js** | Code-only — no visual canvas, no AI, no interactive editor | -| **Draw.io** | Decade-old UX — Limited AI integration, no developer import pipelines | +| **Draw.io** | Decade-old UX — Limited AI integration, no developer import pipelines | | **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 | @@ -91,20 +91,20 @@ OpenFlowKit is the **only MIT-licensed tool** that combines a real workspace hom ## Feature highlights -| | OpenFlowKit | Excalidraw | Draw.io | Mermaid | Lucidchart | -| ------------------------------- | :---------: | :--------: | :-----: | :-----: | :--------: | -| Visual canvas editor | ✅ | ✅ | ✅ | ❌ | ✅ | -| Bidirectional diagram-as-code | ✅ | ❌ | ❌ | ✅ | ❌ | -| AI generation (9 providers) | ✅ | ❌ | ❌ | ❌ | Limited | -| GitHub repo → diagram | ✅ | ❌ | ❌ | ❌ | ❌ | -| SQL → ERD (native parser) | ✅ | ❌ | ❌ | ❌ | ❌ | -| Terraform / K8s / Docker import | ✅ | ❌ | ❌ | ❌ | ❌ | -| AWS / Azure / GCP / CNCF icons | ✅ | ❌ | ✅ | Partial | ✅ | -| Real-time collaboration (P2P) | ✅ | ✅ | ❌ | ❌ | ✅ (cloud) | -| Cinematic animated export | ✅ | ❌ | ❌ | ❌ | ❌ | -| Figma export (editable SVG) | ✅ | ❌ | ❌ | ❌ | ❌ | -| No account required | ✅ | ✅ | ✅ | ✅ | ❌ | -| Open source (MIT) | ✅ | ✅ | ✅ | ✅ | ❌ | +| | OpenFlowKit | Excalidraw | Draw.io | Mermaid | Lucidchart | +| ------------------------------ | :---------: | :--------: | :-----: | :-----: | :--------: | +| Visual canvas editor | ✅ | ✅ | ✅ | ❌ | ✅ | +| Bidirectional diagram-as-code | ✅ | ❌ | ❌ | ✅ | ❌ | +| AI generation (9 providers) | ✅ | ❌ | ❌ | ❌ | Limited | +| GitHub repo → diagram | ✅ | ❌ | ❌ | ❌ | ❌ | +| SQL → ERD (native parser) | ✅ | ❌ | ❌ | ❌ | ❌ | +| Terraform / K8s import | ✅ | ❌ | ❌ | ❌ | ❌ | +| AWS / Azure / GCP / CNCF icons | ✅ | ❌ | ✅ | Partial | ✅ | +| Real-time collaboration (P2P) | ✅ | ✅ | ❌ | ❌ | ✅ (cloud) | +| Cinematic animated export | ✅ | ❌ | ❌ | ❌ | ❌ | +| Figma export (editable SVG) | ✅ | ❌ | ❌ | ❌ | ❌ | +| No account required | ✅ | ✅ | ✅ | ✅ | ❌ | +| Open source (MIT) | ✅ | ✅ | ✅ | ✅ | ❌ | --- @@ -125,17 +125,25 @@ CREATE TABLE orders ( → Typed ERD with inferred foreign-key edges and cardinalities. Rendered in milliseconds, no server involved. ```yaml -# docker-compose.yml -services: - api: - depends_on: [postgres, redis] - postgres: - image: postgres:16 - redis: - image: redis:alpine +# 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 ``` -→ Service architecture with `depends_on` edges and port labels. +→ Kubernetes architecture with Deployment → Service → Ingress connections. **AI-powered imports (API key required):** @@ -143,17 +151,17 @@ services: github.com/vercel/next.js → architecture diagram ``` -→ Fetches the repo, analyzes code structure and dependencies, then generates an editable architecture diagram via AI. Quality depends on the model chosen. +→ Fetches a prioritized slice of the GitHub repository in-browser, analyzes the codebase with AI, then generates an editable architecture diagram. Quality depends on repository size, file coverage, and model choice. | Source | Engine | API key? | | ------------------------- | -------------------------- | :------: | -| GitHub repo URL | AI · 9 languages supported | Yes | +| GitHub repo URL | AI-assisted import | Yes | | SQL DDL | **Native parser** | **No** | | Terraform `.tfstate` | **Native parser** | **No** | | Terraform HCL | AI-assisted | Yes | | Kubernetes YAML / Helm | **Native parser** | **No** | -| Docker Compose | **Native parser** | **No** | -| OpenAPI / Swagger spec | AI-assisted | Yes | +| 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** | @@ -263,14 +271,14 @@ Local-first stays the default. Your saved flows live in the browser, your AI key ## Canvas built for keyboard-first developers -| Shortcut | Action | -| ---------------- | ---------------------------------------------------- | +| Shortcut | Action | +| ---------------- | --------------------------------------------------------- | | `⌘ K` / `Ctrl K` | Command bar — search, import, layout, assets, and actions | -| `⌘ \` / `Ctrl \` | Toggle the live code panel | -| `⌘ Z` / `Ctrl Z` | Full undo with complete history | -| `⌘ D` / `Ctrl D` | Duplicate selection | -| `⌘ G` / `Ctrl G` | Group selected nodes | -| `⌘ /` / `Ctrl /` | Keyboard shortcuts reference | +| `⌘ \` / `Ctrl \` | Toggle the live code panel | +| `⌘ Z` / `Ctrl Z` | Full undo with complete history | +| `⌘ D` / `Ctrl D` | Duplicate selection | +| `⌘ G` / `Ctrl G` | Group selected nodes | +| `⌘ /` / `Ctrl /` | Keyboard shortcuts reference | Plus: smart alignment guides, snap-to-grid, multi-select, pages, layers, sections, architecture lint, light/dark/system theme, and full i18n in 7 languages. @@ -314,13 +322,6 @@ npm run build # upload dist/ to your provider ``` -**Docker:** - -```bash -docker build -t openflowkit . -docker run -p 8080:80 openflowkit -``` - No database. No secrets. No infrastructure. One folder. --- diff --git a/docs-site/.astro/content.d.ts b/docs-site/.astro/content.d.ts index f65e91b..29b61b1 100644 --- a/docs-site/.astro/content.d.ts +++ b/docs-site/.astro/content.d.ts @@ -169,19 +169,531 @@ declare module 'astro:content' { >; type ContentEntryMap = { - + "docs": { +"ai-generation.md": { + id: "ai-generation.md"; + slug: "ai-generation"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"architecture-lint.md": { + id: "architecture-lint.md"; + slug: "architecture-lint"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"ask-flowpilot.md": { + id: "ask-flowpilot.md"; + slug: "ask-flowpilot"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"aws-architecture.md": { + id: "aws-architecture.md"; + slug: "aws-architecture"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"canvas-basics.md": { + id: "canvas-basics.md"; + slug: "canvas-basics"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"choose-export-format.md": { + id: "choose-export-format.md"; + slug: "choose-export-format"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"choose-input-mode.md": { + id: "choose-input-mode.md"; + slug: "choose-input-mode"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"collaboration-sharing.md": { + id: "collaboration-sharing.md"; + slug: "collaboration-sharing"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"command-center.md": { + id: "command-center.md"; + slug: "command-center"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"context-menu.md": { + id: "context-menu.md"; + slug: "context-menu"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"design-systems-branding.md": { + id: "design-systems-branding.md"; + slug: "design-systems-branding"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"diagram-diff.md": { + id: "diagram-diff.md"; + slug: "diagram-diff"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"diagram-families.md": { + id: "diagram-families.md"; + slug: "diagram-families"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"exporting.md": { + id: "exporting.md"; + slug: "exporting"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"figma-design-import.md": { + id: "figma-design-import.md"; + slug: "figma-design-import"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"github-embed.md": { + id: "github-embed.md"; + slug: "github-embed"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"import-from-data.md": { + id: "import-from-data.md"; + slug: "import-from-data"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"infra-sync.md": { + id: "infra-sync.md"; + slug: "infra-sync"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"introduction.md": { + id: "introduction.md"; + slug: "introduction"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"keyboard-shortcuts.md": { + id: "keyboard-shortcuts.md"; + slug: "keyboard-shortcuts"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"local-first-diagramming.md": { + id: "local-first-diagramming.md"; + slug: "local-first-diagramming"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"mermaid-integration.md": { + id: "mermaid-integration.md"; + slug: "mermaid-integration"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"mermaid-vs-openflow.md": { + id: "mermaid-vs-openflow.md"; + slug: "mermaid-vs-openflow"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"mobile-experience.md": { + id: "mobile-experience.md"; + slug: "mobile-experience"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"node-types.md": { + id: "node-types.md"; + slug: "node-types"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"openflow-dsl.md": { + id: "openflow-dsl.md"; + slug: "openflow-dsl"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"payment-flow.md": { + id: "payment-flow.md"; + slug: "payment-flow"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"playback-history.md": { + id: "playback-history.md"; + slug: "playback-history"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"prompting-agents.md": { + id: "prompting-agents.md"; + slug: "prompting-agents"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"properties-panel.md": { + id: "properties-panel.md"; + slug: "properties-panel"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"quick-start.md": { + id: "quick-start.md"; + slug: "quick-start"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"roadmap.md": { + id: "roadmap.md"; + slug: "roadmap"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"settings.md": { + id: "settings.md"; + slug: "settings"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"smart-layout.md": { + id: "smart-layout.md"; + slug: "smart-layout"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"snapshots-recovery.md": { + id: "snapshots-recovery.md"; + slug: "snapshots-recovery"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"studio-overview.md": { + id: "studio-overview.md"; + slug: "studio-overview"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"templates-assets.md": { + id: "templates-assets.md"; + slug: "templates-assets"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"theming.md": { + id: "theming.md"; + slug: "theming"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/ai-generation.md": { + id: "tr/ai-generation.md"; + slug: "tr/ai-generation"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/architecture-lint.md": { + id: "tr/architecture-lint.md"; + slug: "tr/architecture-lint"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/ask-flowpilot.md": { + id: "tr/ask-flowpilot.md"; + slug: "tr/ask-flowpilot"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/aws-architecture.md": { + id: "tr/aws-architecture.md"; + slug: "tr/aws-architecture"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/canvas-basics.md": { + id: "tr/canvas-basics.md"; + slug: "tr/canvas-basics"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/choose-export-format.md": { + id: "tr/choose-export-format.md"; + slug: "tr/choose-export-format"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/choose-input-mode.md": { + id: "tr/choose-input-mode.md"; + slug: "tr/choose-input-mode"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/collaboration-sharing.md": { + id: "tr/collaboration-sharing.md"; + slug: "tr/collaboration-sharing"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/command-center.md": { + id: "tr/command-center.md"; + slug: "tr/command-center"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/design-systems-branding.md": { + id: "tr/design-systems-branding.md"; + slug: "tr/design-systems-branding"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/diagram-diff.md": { + id: "tr/diagram-diff.md"; + slug: "tr/diagram-diff"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/diagram-families.md": { + id: "tr/diagram-families.md"; + slug: "tr/diagram-families"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/exporting.md": { + id: "tr/exporting.md"; + slug: "tr/exporting"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/figma-design-import.md": { + id: "tr/figma-design-import.md"; + slug: "tr/figma-design-import"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/github-embed.md": { + id: "tr/github-embed.md"; + slug: "tr/github-embed"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/import-from-data.md": { + id: "tr/import-from-data.md"; + slug: "tr/import-from-data"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/infra-sync.md": { + id: "tr/infra-sync.md"; + slug: "tr/infra-sync"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/introduction.md": { + id: "tr/introduction.md"; + slug: "tr/introduction"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/keyboard-shortcuts.md": { + id: "tr/keyboard-shortcuts.md"; + slug: "tr/keyboard-shortcuts"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/local-first-diagramming.md": { + id: "tr/local-first-diagramming.md"; + slug: "tr/local-first-diagramming"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/mermaid-integration.md": { + id: "tr/mermaid-integration.md"; + slug: "tr/mermaid-integration"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/mermaid-vs-openflow.md": { + id: "tr/mermaid-vs-openflow.md"; + slug: "tr/mermaid-vs-openflow"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/node-types.md": { + id: "tr/node-types.md"; + slug: "tr/node-types"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/openflow-dsl.md": { + id: "tr/openflow-dsl.md"; + slug: "tr/openflow-dsl"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/payment-flow.md": { + id: "tr/payment-flow.md"; + slug: "tr/payment-flow"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/playback-history.md": { + id: "tr/playback-history.md"; + slug: "tr/playback-history"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/prompting-agents.md": { + id: "tr/prompting-agents.md"; + slug: "tr/prompting-agents"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/properties-panel.md": { + id: "tr/properties-panel.md"; + slug: "tr/properties-panel"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/quick-start.md": { + id: "tr/quick-start.md"; + slug: "tr/quick-start"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/roadmap.md": { + id: "tr/roadmap.md"; + slug: "tr/roadmap"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/smart-layout.md": { + id: "tr/smart-layout.md"; + slug: "tr/smart-layout"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/studio-overview.md": { + id: "tr/studio-overview.md"; + slug: "tr/studio-overview"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/templates-assets.md": { + id: "tr/templates-assets.md"; + slug: "tr/templates-assets"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/theming.md": { + id: "tr/theming.md"; + slug: "tr/theming"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"tr/v1-beta-launch.md": { + id: "tr/v1-beta-launch.md"; + slug: "tr/v1-beta-launch"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +"v1-beta-launch.md": { + id: "v1-beta-launch.md"; + slug: "v1-beta-launch"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".md"] }; +}; + }; type DataEntryMap = { - "docs": Record; - rendered?: RenderedContent; - filePath?: string; -}>; - + }; type AnyEntryMap = ContentEntryMap & DataEntryMap; @@ -213,6 +725,6 @@ declare module 'astro:content' { LiveContentConfig['collections'][C]['loader'] >; - export type ContentConfig = typeof import("../src/content.config.js"); + export type ContentConfig = typeof import("../src/content/config.js"); export type LiveContentConfig = never; } diff --git a/docs-site/src/content/docs/collaboration-sharing.md b/docs-site/src/content/docs/collaboration-sharing.md index 7cce4f1..16168ff 100644 --- a/docs-site/src/content/docs/collaboration-sharing.md +++ b/docs-site/src/content/docs/collaboration-sharing.md @@ -29,6 +29,31 @@ This is important because local-first tools should fail gracefully. If realtime - use room links when you want collaborators in the same canvas context - export JSON when you need a durable editable backup outside the current browser state +## Share Modal Features + +The Share Modal provides: + +- **Room ID**: A unique identifier for the current diagram session +- **Invite URL**: A link others can open to join the room +- **Viewer Count**: Number of people currently in the room +- **Participant Badges**: Visual indicators showing who's in the room (with name and color) +- **Connection Status**: Shows whether you're in realtime sync, connecting, or local-only fallback + +## Connection States + +- **Realtime**: Connected and syncing with other participants in real-time +- **Connecting**: Attempting to establish a realtime connection +- **Fallback**: Working locally; realtime connection not available + +In fallback mode, you can still edit and export your diagram, but changes won't sync with others until reconnected. + +## Best Practices + +1. **Check status before presenting**: Confirm you're in realtime mode when presenting to others +2. **Export before handoff**: Always export JSON when handing off to someone who won't use the room +3. **Use room links for reviews**: Great for live architecture reviews or brainstorming sessions +4. **Local-first by default**: Even without collaboration, your work is always saved locally + ## When to use sharing vs exporting Use collaboration sharing when: diff --git a/docs-site/src/content/docs/context-menu.md b/docs-site/src/content/docs/context-menu.md new file mode 100644 index 0000000..619376f --- /dev/null +++ b/docs-site/src/content/docs/context-menu.md @@ -0,0 +1,91 @@ +--- +draft: false +title: Context Menu & Right-Click Actions +description: Right-click on nodes, edges, or canvas to access quick actions for editing, organizing, and managing diagram elements. +--- + +The context menu appears when you right-click on the canvas, a node, an edge, or multiple selected elements. It provides quick access to common editing operations without using the toolbar or keyboard shortcuts. + +## Canvas Right-Click (Pane Menu) + +Right-click on an empty area of the canvas to see: + +- **Paste**: Paste copied nodes at the cursor position (if clipboard has content) + +## Node Right-Click Menu + +Right-click on a single node to access: + +### Editing + +- **Copy**: Copy the selected node to clipboard +- **Duplicate**: Create an exact copy offset from the original + +### Layer Order + +- **Bring to Front**: Move the node above all other elements +- **Send to Back**: Move the node behind all other elements + +### Section Actions (for Section nodes) + +When right-clicking on a Section node: + +- **Fit Contents**: Resize the section to fit all its children +- **Bring Inside**: Move selected nodes into this section +- **Lock Section**: Prevent editing of section contents +- **Hide Section**: Toggle section visibility + +When a node is inside a section: + +- **Release From Section**: Remove the node from the current section + +### Delete + +- **Delete**: Remove the node from the diagram + +## Edge Right-Click Menu + +Right-click on an edge to access: + +- **Edit Label**: Open inline editing to change the edge label +- **Reverse Direction**: Flip the edge to flow the opposite way +- **Delete Connection**: Remove the edge from the diagram + +## Multi-Select Right-Click Menu + +When multiple nodes are selected and you right-click: + +### Alignment + +A 6-button grid for aligning selected nodes: + +- Align Left / Center / Right +- Align Top / Middle / Bottom + +### Distribution + +- **Distribute Horizontally**: Space nodes evenly from left to right +- **Distribute Vertically**: Space nodes evenly from top to bottom + +### Grouping + +- **Group**: Create a new group containing all selected nodes +- **Wrap in Section**: Create a new section and move all selected nodes into it + +### Delete + +- **Delete**: Remove all selected nodes from the diagram + +## Keyboard Navigation + +The context menu supports keyboard navigation: + +- **Arrow keys**: Navigate between menu items +- **Enter**: Select the focused item +- **Escape**: Close the menu without making a selection + +## Tips + +- The menu auto-positions to stay within the viewport +- For frequently used actions, consider learning the corresponding keyboard shortcuts +- Multi-select alignment is faster with `Shift+Click` to select multiple nodes first diff --git a/docs-site/src/content/docs/diagram-diff.md b/docs-site/src/content/docs/diagram-diff.md index d5bea48..e16edba 100644 --- a/docs-site/src/content/docs/diagram-diff.md +++ b/docs-site/src/content/docs/diagram-diff.md @@ -34,6 +34,21 @@ Compare mode helps with both. It gives you a concrete change view against a know 4. Review the counts for added, removed, and changed elements. 5. Exit compare mode and continue editing if needed. +## Visual Indicators + +When compare mode is active: + +- **Added nodes**: Highlighted with a green indicator +- **Removed nodes**: Highlighted with a red indicator +- **Changed nodes**: Highlighted with a yellow indicator +- **Unchanged elements**: Displayed normally + +The baseline snapshot name and timestamp are shown at the top of the canvas to remind you which version you're comparing against. + +## Exiting Compare Mode + +Click the "Exit Compare" button or press `Escape` to return to normal editing mode. The visual highlights will be removed, and you can continue working on the current diagram. + ## Good use cases - checking the impact of a major Flowpilot revision diff --git a/docs-site/src/content/docs/figma-design-import.md b/docs-site/src/content/docs/figma-design-import.md index bfdba58..06adef9 100644 --- a/docs-site/src/content/docs/figma-design-import.md +++ b/docs-site/src/content/docs/figma-design-import.md @@ -9,10 +9,17 @@ OpenFlowKit includes a Figma import flow for design-system work. Instead of recr ## What you need - a Figma file URL -- a personal access token +- a Figma personal access token The token is used in your browser to fetch styles. This is a token-based import flow, not a permanent synced integration. +### Getting a Figma Token + +1. Go to Figma Settings > Account +2. Scroll to "Personal access tokens" +3. Create a new token with a descriptive name +4. Copy the token (it won't be shown again) + ## What the import previews The current import flow can preview: diff --git a/docs-site/src/content/docs/infra-sync.md b/docs-site/src/content/docs/infra-sync.md index daf72fd..a6cc392 100644 --- a/docs-site/src/content/docs/infra-sync.md +++ b/docs-site/src/content/docs/infra-sync.md @@ -31,11 +31,17 @@ That distinction matters for trust. If the goal is to stay close to the underlyi 1. Open Studio. 2. Switch to the **Infra** tab. -3. Select the matching format. -4. Paste or drop the file contents. -5. Generate the diagram. -6. Review the summary. -7. Apply it to the canvas. +3. Select the matching format (Terraform State, Kubernetes YAML, or Docker Compose). +4. Paste or drop the file contents into the text area. +5. Click "Generate" to create the diagram. +6. Review the summary showing nodes and connections found. +7. Click "Apply" to add the diagram to the canvas. + +### Supported File Types + +- **Terraform State** (`.tfstate`): Parses resources and their relationships from Terraform state output +- **Kubernetes YAML**: Parses deployments, services, pods, and their connections +- **Docker Compose YAML**: Parses services, networks, and volumes ## After import diff --git a/docs-site/src/content/docs/mobile-experience.md b/docs-site/src/content/docs/mobile-experience.md new file mode 100644 index 0000000..76a02ea --- /dev/null +++ b/docs-site/src/content/docs/mobile-experience.md @@ -0,0 +1,83 @@ +--- +draft: false +title: Mobile Experience +description: Using OpenFlowKit on tablets and phones — capabilities, limitations, and best practices. +--- + +OpenFlowKit is designed primarily for desktop browsers, but it provides a functional experience on tablets and phones. + +## Supported Devices + +OpenFlowKit works on: + +- **Tablets** (iPad, Android tablets): Full functionality with touch interactions +- **Phones**: Basic viewing and editing with some limitations + +## What Works on Mobile + +### Viewing Diagrams + +- Pan and zoom on the canvas +- Open saved diagrams from the home screen +- View exported diagrams + +### Basic Editing + +- Tap nodes to select them +- Drag nodes to reposition +- Add new nodes via touch gestures +- Access the context menu via long-press + +### Export & Share + +- Export diagrams to PNG, SVG, JSON +- Share via viewer links +- Open export menu from the toolbar + +## Limitations on Mobile + +### Not Supported + +- **Precise alignment**: Touch precision is limited for fine-grained positioning +- **Multi-select box**: Drag-to-select is not available +- **Keyboard shortcuts**: External keyboards may work but are not optimized +- **Context menus**: Some right-click actions may be harder to access + +### Reduced Functionality + +- **Properties Panel**: Editing node properties works but is less convenient +- **Command Bar**: Search and command features work with on-screen keyboard +- **Asset Browser**: Cloud provider icons and asset libraries are harder to browse +- **AI Generation**: Flowpilot works but entering prompts is slower + +### Performance + +- Large diagrams may be slower to render +- Autosave still works but may have slightly higher latency + +## Recommended Workflows for Mobile + +1. **Review and present**: OpenFlowKit is great for viewing diagrams on mobile during meetings or code reviews +2. **Quick edits**: Simple changes like moving nodes or editing labels work well +3. **Export on-the-go**: Export diagrams to include in documents or presentations + +## For Full Functionality + +For the best experience, use a desktop browser: + +- Chrome, Firefox, Safari, or Edge on macOS or Windows +- An external keyboard for shortcut access +- A mouse or trackpad for precise selection + +## Tips + +- Pinch-to-zoom works naturally on touch devices +- Double-tap to zoom in on a specific area +- Use the minimap to navigate large diagrams +- The toolbar is touch-friendly with larger tap targets + +## Related Pages + +- [Canvas Basics](/canvas-basics/) +- [Properties Panel](/properties-panel/) +- [Exporting](/exporting/) diff --git a/docs-site/src/content/docs/roadmap.md b/docs-site/src/content/docs/roadmap.md index a21bd1d..79db865 100644 --- a/docs-site/src/content/docs/roadmap.md +++ b/docs-site/src/content/docs/roadmap.md @@ -16,6 +16,25 @@ The current product direction is built around a few clear pillars that already s - asset libraries for developer, cloud, and icon-heavy diagrams - design systems, pages, layers, and structured canvas controls +## Recently shipped + +These capabilities have been released and are documented in the docs: + +- **Workspace Home**: Create, open, import, and organize multiple flows +- **Local-First Storage**: All diagrams saved in browser, survives refresh +- **Flowpilot AI**: Generate diagrams from prompts +- **Mermaid Import**: Import and edit Mermaid diagrams +- **OpenFlow DSL**: Text-based diagram definition language +- **Infrastructure Sync**: Import Terraform, Kubernetes, Docker Compose +- **Smart Layout**: Automatic arrangement of nodes +- **Playback History**: Step through diagram changes +- **Snapshots**: Save and restore named versions +- **Diagram Diff**: Compare current state against snapshots +- **Architecture Linting**: Check diagrams for architectural rules +- **Context Menu**: Right-click actions for nodes, edges, selections +- **Settings Modal**: Configure AI, canvas, and keyboard shortcuts +- **Multiple Diagram Families**: Flowchart, State, Class, ER, GitGraph, Mindmap, Journey, Architecture + ## Near-term roadmap These are the highest-signal improvements currently worth planning around: diff --git a/docs-site/src/content/docs/settings.md b/docs-site/src/content/docs/settings.md new file mode 100644 index 0000000..5995356 --- /dev/null +++ b/docs-site/src/content/docs/settings.md @@ -0,0 +1,66 @@ +--- +draft: false +title: Settings & Preferences +description: Configure OpenFlowKit to match your workflow — AI providers, canvas behavior, and keyboard shortcuts. +--- + +OpenFlowKit offers customizable settings across three areas: Canvas preferences, AI configuration, and Keyboard shortcuts. + +## Accessing Settings + +Click the **Settings** option from the home screen navigation, or press `Cmd+,` (Mac) / `Ctrl+,` (Windows) when inside an editor to open the Settings modal. + +## Canvas Settings + +The Canvas tab controls how the editor behaves: + +- **Snap to Grid**: Toggle whether nodes snap to a grid when moved +- **Snap to Objects**: Toggle whether nodes snap to other nodes and edges +- **Auto-fit View**: Choose whether the canvas automatically fits content on load +- **Minimap**: Toggle the minimap visibility in the bottom corner +- **Connection Line**: Choose between smoothstep, straight, or bezier connection lines + +These preferences persist across sessions and apply to all diagrams. + +## AI Settings + +The AI tab configures how Flowpilot generates diagrams: + +### Supported Providers + +- **OpenAI** (GPT-4o, GPT-4o mini) +- **Anthropic** (Claude 3.5 Sonnet, Claude 3 Haiku) +- **Google** (Gemini 1.5 Pro, Gemini 1.5 Flash) + +### Configuration Options + +1. **Select Provider**: Choose your preferred AI provider from the dropdown +2. **Enter API Key**: Paste your API key for the selected provider +3. **Key Persistence**: Choose whether the key persists across browser sessions or is cleared when the tab closes + +If you don't have an API key, visit the provider's website to create one. OpenFlowKit does not require any server-side configuration — all AI requests go directly from your browser to the provider. + +### Troubleshooting + +- **Key not working**: Verify the key is valid and has API access +- **Rate limits**: Check the provider's dashboard for usage limits +- **Model availability**: Some models may not be available in all regions + +## Keyboard Shortcuts + +The Shortcuts tab displays all available keyboard shortcuts organized by category: + +- **Essentials**: Undo, redo, select all, delete, clear selection +- **Manipulation**: Multi-select, selection box, duplicate, copy, paste +- **Nodes**: Mindmap navigation, rename, quick create +- **Navigation**: Select tool, hand tool, pan, zoom, fit view +- **Help**: Keyboard shortcuts modal, command bar, search + +Shortcuts automatically adapt to Mac or Windows — `Cmd` becomes `Ctrl` on Windows, and `Opt` becomes `Alt`. + +## Related Pages + +- [Quick Start](/quick-start/) +- [Keyboard Shortcuts](/keyboard-shortcuts/) +- [AI Generation](/ai-generation/) +- [Ask Flowpilot](/ask-flowpilot/) diff --git a/docs-site/src/content/docs/snapshots-recovery.md b/docs-site/src/content/docs/snapshots-recovery.md new file mode 100644 index 0000000..1b71595 --- /dev/null +++ b/docs-site/src/content/docs/snapshots-recovery.md @@ -0,0 +1,79 @@ +--- +draft: false +title: Snapshots & Recovery +description: Save named versions of your diagram and restore to previous states when things go wrong. +--- + +Snapshots let you save specific points in your diagram's history and restore to them later. This is different from the automatic undo history — snapshots are named, persistent, and survive browser refreshes. + +## When to Use Snapshots + +- **Before major changes**: Save before an AI rewrite, large import, or significant restructure +- **Experimentation**: Create a baseline before trying different approaches +- **Collaboration**: Mark completed states before handing off to others +- **Recovery**: Restore a known-good state if something goes wrong + +## Creating a Snapshot + +1. Open the **Snapshots Panel** from the toolbar or Studio rail +2. Enter a name in the version name field +3. Click the save button + +The snapshot saves the current state including all nodes, edges, and their properties. + +## Viewing Snapshots + +The Snapshots Panel shows two sections: + +### Named Versions + +Snapshots you created manually with custom names. These are persistent and won't be automatically deleted. + +### Autosaved Checkpoints + +Automatic snapshots created by the system: + +- Before major operations like imports or AI generations +- At regular intervals during editing sessions +- These help you recover from unexpected issues + +## Restoring a Snapshot + +1. Find the snapshot in the Snapshots Panel +2. Click the restore button on the card +3. The diagram reverts to that snapshot's state +4. You can continue editing from there + +Restoring does not delete other snapshots — you can always restore a different one later. + +## Comparing with Current State + +1. Find the snapshot you want to compare +2. Click the compare button +3. The diagram enters compare mode showing: + - Nodes that were added (green) + - Nodes that were removed (red) + - Nodes that were modified (yellow) +4. Exit compare mode to continue editing + +See [Diagram Diff & Compare](/diagram-diff/) for more on compare mode. + +## Deleting Snapshots + +- Click the delete button on a snapshot card to remove it +- Autosaved checkpoints can be deleted to clean up the list +- Deleted snapshots cannot be recovered + +## Best Practices + +1. **Name snapshots meaningfully**: "Before AI rewrite v2" is better than "Version 2" +2. **Create before risky operations**: Always snapshot before import, AI generation, or batch edits +3. **Use autosaved checkpoints**: They're helpful fallbacks but don't rely on them alone +4. **Clean up old snapshots**: Delete outdated snapshots to keep the list manageable + +## Related Pages + +- [Playback & History](/playback-history/) +- [Diagram Diff & Compare](/diagram-diff/) +- [Import from Structured Data](/import-from-data/) +- [AI Generation](/ai-generation/) diff --git a/docs/internal/COMPETITIVE_ANALYSIS.md b/docs/internal/COMPETITIVE_ANALYSIS.md deleted file mode 100644 index d9aa93e..0000000 --- a/docs/internal/COMPETITIVE_ANALYSIS.md +++ /dev/null @@ -1,273 +0,0 @@ -# OpenFlowKit — Competitive Analysis + Launch Roadmap - -**Last updated:** 2026-03-26 -**Audience:** Founder. Unfiltered. -**Status:** Rewritten against the current codebase, not old assumptions. - ---- - -## Who We Are Actually Building For - -Primary audience: -- developers -- technical builders -- DevRel / technical writers -- startup teams shipping technical products - -Secondary audience: -- PMs and designers who work closely with technical systems - -This matters because our strongest wedge is not "general diagramming for everyone." -It is: - -> **local-first diagramming for builders who need structure, portability, privacy, and AI-assisted workflows** - -The product can serve broader users, but the launch story is strongest when we lead with the technical builder use case. - ---- - -## Verified Current State - -### Clearly Shipped - -These are real in the codebase today: - -- Welcome flow with template/import/blank entry points -- Inline AI key setup in the studio panel -- Cinematic animated export in the product UI as video and GIF -- Sequence diagrams with plugin support, Mermaid import/export, property-panel support, and starter template coverage -- `/view` route and share viewer flow -- Template system with launch-priority metadata -- SQL import, Terraform/Kubernetes/Docker Compose infrastructure parsing, Mermaid import/export, OpenFlow JSON document import/export -- Architecture linting with rule library/templates -- Playback/presentation system -- Collaboration beta -- Figma export and Figma style import - -### Shipped But Still Needs Polish - -- Welcome flow is better than before, but the builder-first path can still be more explicit. -- AI setup works, but the local-first / BYOK explanation can still be clearer. -- Template gallery has strong foundations, but the launch-facing set is still thin relative to the opportunity. -- Share/embed exists, but the output and promotion surface are still not strong enough. -- Cinematic export direction is strong, but visual polish and export performance still have room to improve. - -### Partially Shipped - -- Sequence is no longer missing. It is partially complete and already useful, but still deserves a completion audit before aggressive claims. -- C4 and network support exist in meaningful form through architecture resource types, starter templates, asset categories, and lint templates, but the breadth is still below dedicated enterprise diagram suites. -- Local-first persistence is real, but cold-start offline app-shell support is still not defensible. - -### Not Shipped Yet - -- true offline PWA/app-shell caching -- larger enterprise-oriented shape breadth -- scale/performance work specifically for very large diagrams -- stronger embed preset system for README/card/full share formats - ---- - -## Verified By Code Inspection - -### What is actually true right now - -| Feature | Status | Notes | -|---|---|---| -| Welcome onboarding | ✅ | Builder-oriented starter templates/import prompts are already configured in `src/services/onboarding/config.ts` | -| Inline AI setup | ✅ | Present in `StudioAIPanel` and wired into the main studio flow | -| Cinematic export | ✅ | Current animated export path is cinematic-only in the UI | -| Sequence diagrams | ✅ | Visual node/edge types, property panels, Mermaid export, parser plugin, starter template | -| C4 architecture support | ✅ partial | Resource types, templates, lint library support exist | -| Network/infra support | ✅ partial | Network resource types, templates, provider catalogs, infra parsers exist | -| README/share viewer | ✅ | `/view` route and share modal flow exist | -| Local-first persistence | ✅ | Stored locally; no server account model required | -| Full offline cold-start | ❌ | No defensible service-worker app-shell layer yet | - -### Important corrections to old assumptions - -- Sequence is **not** a missing feature anymore. -- Animated export is **not** "playback/reveal first" anymore in the product UI. The product now exposes cinematic export. -- C4 and network support are **not** fully absent. They exist, but are still shallow compared with the eventual target. -- The product is stronger for technical builders than the old "broad builders" framing admitted. - ---- - -## Competitive Read - -### Where we are genuinely strong - -1. **AI breadth + privacy** -We have a wider practical AI surface than most diagram tools, and we do it with a BYOK/local-first posture that is genuinely differentiated. - -2. **Export portability** -PNG, SVG, PDF, Mermaid, PlantUML, JSON/OpenFlow, Figma, and cinematic animated output is an unusually strong export surface. - -3. **Structured technical diagram depth** -ER, architecture, class, state, journey, mindmap, flowchart, and sequence give us a better technical-builder mix than the usual whiteboard-first tools. - -4. **Local-first product story** -No account, no default server storage, portable artifacts, and private workflows are a meaningful wedge. - -### Where competitors still feel stronger - -1. **Activation and immediate clarity** -FigJam, Excalidraw, and even Lucidchart feel clearer in the first minute. - -2. **Template breadth** -Lucidchart and draw.io win on sheer catalog volume. - -3. **Visual polish** -We are better than before, but still not at the level where people immediately describe the product as premium or inevitable. - -4. **Cold-start offline** -We can honestly claim local-first persistence. We should not yet over-claim full offline web-app resilience. - ---- - -## Honest Gaps - -### Gap 1 — Activation polish - -Still the highest-priority product gap. - -What remains: -- make the fastest builder paths even more obvious -- reduce any ambiguity between template, import, and AI-first starts -- ensure the first useful artifact happens in under 2 minutes - -Why it matters: -- this directly affects Product Hunt conversion -- this directly affects Hacker News patience -- this is more important than adding breadth right now - -### Gap 2 — AI setup clarity - -The capability is real. The explanation can still be sharper. - -What remains: -- clearer provider guidance -- clearer local-first / key storage explanation -- clearer “what to do next” immediately after a key is saved - -Why it matters: -- AI is one of our biggest moats -- if setup feels unclear, the moat is invisible - -### Gap 3 — Sequence completion audit - -Sequence is already in the product, but it should be treated as a completion/polish project. - -What remains: -- verify create/edit/export/re-import paths end to end -- ensure discovery is strong enough from app entry points -- tighten starter template and documentation support - -Why it matters: -- sequence is highly searched by backend/API users -- under-claiming a real feature is better than overstating a partial one - -### Gap 4 — Share/embed quality - -The underlying share system exists. The output is not yet strong enough to become a distribution loop. - -What remains: -- better viewer presets -- better Markdown/embed snippets -- more explicit promotion of shareable outputs - -Why it matters: -- this is the bridge from “good product” to “artifacts that spread” - -### Gap 5 — Template gallery depth - -The system exists. The catalog is the weak point. - -What remains: -- more curated technical-builder templates -- stronger naming, descriptions, and launch-quality examples -- a better “first page” of templates for common builder tasks - -Why it matters: -- this improves activation -- this improves SEO/content capture -- this reduces blank-canvas friction - -### Gap 6 — Offline/PWA defensibility - -Current honest statement: -- local-first persistence: yes -- works without a backend for many flows: yes -- guaranteed cold-start offline web app: no - -What remains: -- app-shell caching -- service-worker strategy -- removal of remaining cold-start external dependencies from the critical offline path - -### Gap 7 — Performance at scale - -Current work has improved structure and some export performance, but large-diagram behavior still needs direct profiling and targeted optimization. - -What remains: -- large graph profiling -- path/render cost reduction -- export performance follow-up - ---- - -## Outdated Claims Removed - -These were stale and should no longer guide planning: - -- "Sequence must build from scratch" -- "Cinematic reveal export needs to be invented" -- "C4/network are not started at all" -- "Playback/reveal export menu is the current product surface" - -These are no longer true enough to use as roadmap anchors. - ---- - -## Recommended Execution Order - -### Next 2 Weeks - -1. Activation polish -2. AI setup clarity -3. Sequence completion audit -4. Share/embed preset quality -5. Template gallery expansion - -### After That - -1. Offline/PWA hardening -2. Performance-at-scale work -3. Broader C4/network depth -4. Additional enterprise-oriented shape breadth - ---- - -## What I Would Improve In This Doc Later - -This version is now a much better source of truth, but it could still improve by adding: - -- a stricter scoring table per pending item -- a launch-only subset versus a post-launch subset -- explicit owner/scope per item -- direct links from each roadmap bullet to the code surface that supports it - ---- - -## Bottom Line - -The product is stronger than the older roadmap implied. - -The biggest remaining work is not "invent major new capability." -It is: - -- finish and sharpen what already exists -- make the best features easier to discover -- make the resulting artifacts easier to share -- only then broaden further - -That is a much better position than being early on fundamentals. diff --git a/scripts/analyze-bundle.mjs b/scripts/analyze-bundle.mjs new file mode 100644 index 0000000..c6da288 --- /dev/null +++ b/scripts/analyze-bundle.mjs @@ -0,0 +1,191 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const DIST_DIR = path.resolve(process.cwd(), 'dist'); +const ASSETS_DIR = path.join(DIST_DIR, 'assets'); +const REPORT_PATH = path.join(DIST_DIR, 'bundle-report.html'); + +function toKb(bytes) { + return Number((bytes / 1024).toFixed(1)); +} + +function readAssetEntries() { + if (!fs.existsSync(ASSETS_DIR)) { + throw new Error('Missing dist/assets. Run "npm run build" before "npm run build:analyze".'); + } + + return fs.readdirSync(ASSETS_DIR) + .map((filename) => { + const filePath = path.join(ASSETS_DIR, filename); + const stats = fs.statSync(filePath); + const extension = path.extname(filename).slice(1) || 'other'; + + return { + filename, + extension, + sizeBytes: stats.size, + sizeKb: toKb(stats.size), + }; + }) + .sort((left, right) => right.sizeBytes - left.sizeBytes); +} + +function renderReport(entries) { + const totalBytes = entries.reduce((sum, entry) => sum + entry.sizeBytes, 0); + const totalKb = toKb(totalBytes); + const rows = entries.map((entry) => { + const width = totalBytes === 0 ? 0 : Math.max((entry.sizeBytes / totalBytes) * 100, 1); + return ` + + + + + + + `; + }).join(''); + + return ` + + + + OpenFlowKit Bundle Report + + + +
+

Bundle Report

+

Generated from dist/assets. Use this after builds to spot chunk drift and oversized route payloads quickly.

+
+
+
Total Assets
+
${entries.length}
+
+
+
Total Size
+
${totalKb} KB
+
+
+
Largest Asset
+
${entries[0]?.sizeKb ?? 0} KB
+
+
+
🏠 Workspace Home
Create · open · import
No forced blank file
🧑‍💻 Code → Diagram
GitHub · SQL · Terraform
K8s · Docker Compose
🧑‍💻 Code → Diagram
GitHub · SQL · Terraform
K8s · OpenAPI
🤖 AI Generation
9 providers · BYOK
Streaming diff preview
`{}` Diagram as Code
Bidirectional live sync
Git-friendly DSL
🧩 Asset Libraries
Developer · AWS · Azure
GCP · CNCF · Icons
${entry.filename}${entry.extension.toUpperCase()}${entry.sizeKb} KB +
+
+
+
+ + + + + + + + + ${rows} +
AssetTypeSizeShare
+ + +`; +} + +function main() { + const entries = readAssetEntries(); + fs.writeFileSync(REPORT_PATH, renderReport(entries), 'utf8'); + console.log(`Bundle report written to ${REPORT_PATH}`); +} + +main(); diff --git a/scripts/translate-all.mjs b/scripts/translate-all.mjs new file mode 100644 index 0000000..6edb0e3 --- /dev/null +++ b/scripts/translate-all.mjs @@ -0,0 +1,443 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = process.cwd(); +const LOCALES = path.join(ROOT, 'src', 'i18n', 'locales'); + +function readJson(p) { + return JSON.parse(fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, '')); +} +function writeJson(p, data) { + fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf8'); +} + +function flatten(obj, prefix = '', out = {}) { + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return out; + for (const [k, v] of Object.entries(obj)) { + const key = prefix ? prefix + '.' + k : k; + if (typeof v === 'string') out[key] = v; + else if (typeof v === 'object' && !Array.isArray(v)) flatten(v, key, out); + } + return out; +} + +function getByPath(obj, dotted) { + return dotted + .split('.') + .reduce((acc, k) => (acc && typeof acc === 'object' && k in acc ? acc[k] : undefined), obj); +} +function setByPath(obj, dotted, value) { + const parts = dotted.split('.'); + let cur = obj; + for (let i = 0; i < parts.length - 1; i++) { + if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object' || Array.isArray(cur[parts[i]])) + cur[parts[i]] = {}; + cur = cur[parts[i]]; + } + cur[parts[parts.length - 1]] = value; +} +function deleteByPath(obj, dotted) { + const parts = dotted.split('.'); + let cur = obj; + for (let i = 0; i < parts.length - 1; i++) { + if (!cur || typeof cur !== 'object' || !(parts[i] in cur)) return; + cur = cur[parts[i]]; + } + if (cur && typeof cur === 'object') delete cur[parts[parts.length - 1]]; +} + +// ─── Turkish translations ─── +const tr = { + 'common.rename': 'Yeniden Adlandır', + 'nav.beta': 'BETA', + 'nav.privacyMessage': 'Diyagramlarınız sizinle kalır ve sunucularımıza ulaşmaz.', + 'export.headingFormat': 'Dışa Aktarma Biçimi', + 'export.svg': 'SVG Olarak Dışa Aktar', + 'export.video': 'Oynatma Videosu', + 'export.gif': 'Oynatma GIF', + 'export.revealVideo': 'Videoyu Göster', + 'export.revealGif': 'GIF Göster', + 'export.cinematicVideo': 'Sinematik Yapım Videosu', + 'export.cinematicGif': 'Sinematik Yapım GIF', + 'export.share': 'Canlı tuvali paylaş', + 'export.actionDownload': 'İndir', + 'export.exportOrShare': 'Bu tuvali dışa aktar veya paylaş', + 'export.shareSection': 'Tuvali paylaş', + 'export.openflowdslLabel': '{{appName}} DSL', + 'export.hintSvgScalable': 'Ölçeklenebilir vektör dosyası', + 'export.hintPlaybackWebM': 'Oynatma zaman çizelgesi (WebM/MP4)', + 'export.hintPlaybackGif': 'Belgeler/sosyal medya için kısa döngü', + 'export.hintRevealVideo': 'Düğümler sırayla belirir (WebM/MP4)', + 'export.hintRevealGif': 'Belgeler/sosyal medya için animasyonlu gösterim', + 'export.hintCinematicVideo': 'Karanlık lansman tarzı yapım', + 'export.hintCinematicGif': 'Karanlık sosyal medya döngüsü', + 'export.hintShareViewer': 'Bu oda için davet bağlantısı', + 'export.readmeEmbed': 'README Yerleşimi', + 'export.hintReadmeEmbed': 'Markdown snippet kopyala', + 'export.sectionImage': 'Görsel', + 'export.sectionVideo': 'Video ve Animasyon', + 'export.sectionCode': 'Kod ve Veri', + 'nodes.mindmap': 'Konu', + 'nodes.text': 'Metin', + 'nodes.image': 'Görsel', + 'nodes.group': 'Grup', + 'nodes.items': 'öğe', + 'landing.nav.figma': 'Figma', + 'landing.hero.publicBeta': 'v1.0 Genel Beta', + 'landing.pricing.openSource': 'Açık Kaynak', + 'home.autosaved': 'Otomatik kaydedildi', + 'home.currentFlow': 'Mevcut akış', + 'home.localStorageHint': + 'Bu cihazda otomatik kaydedildi. Diyagram verilerinizi sunucularımıza yüklemiyoruz.', + 'home.renameFlow.title': 'Akışı yeniden adlandır', + 'home.renameFlow.description': 'Gösterge panelinizde ve editörde görünen adı güncelleyin.', + 'home.renameFlow.label': 'Akış adı', + 'home.renameFlow.placeholder': 'Bir akış adı girin', + 'home.renameFlow.hint': + 'Adlar, dışa aktarmadığınız veya başka bir yerde senkronize etmediğiniz sürece bu tarayıcı profilinde yereldir.', + 'home.renameFlow.closeDialog': 'Akış yeniden adlandırma iletişim kutusunu kapat', + 'home.deleteFlow.title': 'Akışı sil', + 'home.deleteFlow.description': 'Bu, yerel otomatik kaydedilen akışı bu cihazdan kaldırır.', + 'home.deleteFlow.confirmation': '"{{name}}" silinsin mi?', + 'home.deleteFlow.hint': + 'Dışa aktarılmış bir yedeklemeniz veya başka bir kopyanız yoksa bu işlem geri alınamaz.', + 'home.deleteFlow.closeDialog': 'Akış silme iletişim kutusunu kapat', + 'settings.themeLight': 'Açık', + 'settings.themeDark': 'Koyu', + 'settings.themeSystem': 'Sistem', + 'properties.title': 'Özellikler', + 'properties.shape': 'Şekil', + 'properties.color': 'Renk', + 'properties.icon': 'İkon', + 'properties.rotation': 'Döndürme', + 'properties.transparency': 'Saydamlık', + 'properties.imageSettings': 'Görsel Ayarları', + 'properties.bulkShape': 'Toplu Şekil', + 'properties.bulkColor': 'Toplu Renk', + 'properties.bulkIcon': 'Toplu İkon', + 'properties.labelTransform': 'Etiket Dönüşümü', + 'properties.findReplace': 'Bul ve Değiştir', + 'properties.findLabel': 'Bul', + 'properties.findPlaceholder': 'Bulunacak metin', + 'properties.replaceLabel': 'Değiştir', + 'properties.replacePlaceholder': 'Değiştirilecek metin', + 'properties.prefixOptional': 'Önek (isteğe bağlı)', + 'properties.suffixOptional': 'Sonek (isteğe bağlı)', + 'properties.useRegex': 'Düzenli ifade kullan', + 'properties.applyToSelectedNodes': 'Seçili öğelere uygula', + 'properties.selectFieldToApply': 'Uygulamak için bir alan seçin', + 'properties.previewSummary': 'Önizleme özeti', + 'ai.generateWithFlowpilot': 'Flowpilot ile oluştur', + 'ai.model': 'Model', + 'ai.settingsSubtitle': + 'Tercih ettiğiniz yapay zeka sağlayıcısını, modeli ve API anahtarını aşağıdan yapılandırın.', + 'welcome.title': 'OpenFlowKit', + 'welcome.feature1Title': 'Harika diyagramlar oluşturun', + 'welcome.feature1Desc': 'Güzel, kurumsal düzeyde mimariyi görsel olarak tasarlayın.', + 'welcome.feature2Title': 'Yapay zeka kullanın', + 'welcome.feature2Desc': 'Tek bir akıllı komutla tam mimariler oluşturun.', + 'welcome.feature3Title': 'Koddan diyagrama', + 'welcome.feature3Desc': 'Metinden anında muhteşem görsel altyapı oluşturun.', + 'welcome.feature4Title': 'Birçok formatta dışa aktarın', + 'welcome.feature4Desc': 'Tamamen animasyonlu sunum diyagramlarına dışa aktarın.', + 'welcome.analyticsTitle': 'Anonim Analitik', + 'welcome.analyticsDesc': + 'Tanı verileri topluyoruz. Diyagramlarınızı veya komutlarınızı asla okumuyoruz.', + 'welcome.getStarted': 'Hemen Başla', + 'cta.github': 'GitHub', + 'share.betaBadge': 'Beta', + 'share.roomId': 'Oda Kimliği', + 'share.link': 'Bağlantı paylaş', + 'share.copied': 'Bağlantı Kopyalandı!', + 'share.close': 'Kapat', + 'share.openDialog': 'Paylaşım diyaloğu', + 'share.toast.copyManual': + 'Pano erişimi engellendi. Bağlantıyı paylaşım diyaloğundan manuel olarak kopyalayın.', + 'share.toast.fallbackMode': + 'Gerçek zamanlı senkronizasyon kullanılamıyor. Yalnızca yerel modda devam ediliyor.', + 'share.toast.reconnected': 'Gerçek zamanlı iş birliği geri yüklendi.', + 'share.status.cache.syncing': ' yerel önbellek senkronize ediliyor', + 'share.status.cache.ready': ' yerel önbellek hazır', + 'share.status.cache.hydrated': ' yerel önbellekten geri yüklendi', + 'share.roomLink': 'İş Birliği Bağlantısı', + 'share.permissionsNote': 'Bağlantıya sahip olan herkes katılabilir.', + 'share.permissionsNoteSecondary': + 'İzin kontrolleri ve kalıcı arka uç senkronizasyonu henüz yapılandırılmadı.', + 'share.viewerCount.one': 'Bu oturumda 1 izleyici.', + 'share.viewerCount.many': 'Bu oturumda {{count}} izleyici.', + 'share.mode.realtime.title': 'Gerçek zamanlı senkronizasyon aktif', + 'share.mode.realtime.body': + 'Bu bağlantıyı açan akranlar, mevcut aktarım üzerinden canlı güncellemeleri görebilir.', + 'share.mode.waiting.title': 'Gerçek zamanlı senkronizasyona bağlanılıyor', + 'share.mode.waiting.body': + 'Bu tuval hâlâ canlı eş senkronizasyonu kurmaya çalışıyor. Başarısız olursa oturum bu tarayıcıda yalnızca yerel kalır.', + 'share.mode.fallback.title': 'Yalnızca yerel iş birliği', + 'share.mode.fallback.body': + 'Arka uç aktarıcı veya desteklenen eş aktarımı olmadan bu oturum, mevcut tarayıcı çalışma zamanı dışında kalıcı çok kullanıcılı canlı senkronizasyon sağlamaz.', + 'share.cache.syncing.title': 'Yerel oda önbelleği senkronize ediliyor', + 'share.cache.syncing.body': + 'Bu tarayıcı, eş senkronizasyon tamamen oturana kadar IndexedDB önbellekli oda durumunu geri yüklemeye devam ediyor.', + 'share.cache.hydrated.title': 'Yerel önbellekten kurtarıldı', + 'share.cache.hydrated.body': + 'Bu odada zaten bu tarayıcıda yerel olarak önbelleklenmiş durum vardı, bu yüzden tuval eşler yeniden bağlanmadan önce geri yüklenebildi.', + 'share.cache.ready.title': 'Yerel önbellek hazır', + 'share.cache.ready.body': + 'Bu tarayıcı, yeniden yükleme ve çevrimdışı kurtarma için odanın yerel bir IndexedDB kopyasını tutabilir.', + 'share.cache.unavailable.title': 'Yerel oda önbelleği yok', + 'share.cache.unavailable.body': + 'Bu iş birliği oturumu şu anda tarayıcı IndexedDB oda kalıcılığını kullanmıyor.', + 'chatbot.aiSuffix': 'Yapay Zeka...', + 'commandBar.ai.examples.cicdPipeline': 'CI/CD Hattı', + 'commandBar.search.showingCount': 'Gösterilen: {{count}}', + 'commandBar.search.totalCount': 'Tuvaldeki toplam: {{count}}', + 'commandBar.figmaImport.fileUrlPlaceholder': 'https://www.figma.com/design/...', + 'commandBar.figmaImport.tokenPlaceholder': 'figd_...', + 'commandBar.import.parseNativeProject': 'Yerel Diyagram Oluştur', + 'commandBar.import.categories.sql': 'SQL', + 'commandBar.import.categories.infra': 'Altyapı', + 'commandBar.import.categories.openapi': 'OpenAPI', + 'commandBar.import.categories.code': 'Kod', + 'commandBar.import.infraFormats.terraformState': 'Terraform State (.tfstate)', + 'commandBar.import.infraFormats.kubernetes': 'Kubernetes YAML', + 'commandBar.import.infraFormats.dockerCompose': 'Docker Compose', + 'commandBar.import.infraFormats.terraformHcl': 'Terraform HCL (YZ)', + 'commandBar.layout.layoutStyle': 'Düzen Stili', + 'commandBar.layout.normal': 'Normal', + 'commandBar.visuals.bezier': 'Bezier', + 'commandBar.visuals.largeGraphSafety': 'Büyük Graf Güvenliği', + 'commandBar.visuals.largeGraphSafetyAuto': 'Otomatik', + 'commandBar.visuals.largeGraphSafetyOff': 'Kapalı', + 'commandBar.visuals.exportMode': 'Dışa Aktarma Modu', + 'commandBar.visuals.exportModeDeterministic': 'Kesin', + 'commandBar.visuals.exportModeLegacy': 'Eski', + 'commandBar.code.jumpToLine': '{{line}} satırına git', + 'commandBar.code.diagnosticsGroup.syntax': 'Sözdizimi sorunları', + 'commandBar.code.diagnosticsGroup.identity': 'Tanımlayıcı sorunları', + 'commandBar.code.diagnosticsGroup.recovery': 'Kurtarma uyarıları', + 'commandBar.code.diagnosticsGroup.general': 'Tanılar', + 'settingsModal.settings': 'Ayarlar', + 'settingsModal.description': 'Tuval tercihlerini ve klavye kısayollarını yapılandırın.', + 'settingsModal.close': 'Ayarları kapat', + 'settingsModal.closeDialog': 'Ayarlar iletişim kutusunu kapat', + 'settingsModal.canvasSettings': 'Tuval Ayarları', + 'settingsModal.ai.model': 'Model', + 'settingsModal.ai.optional': 'İsteğe bağlı', + 'settingsModal.ai.privacyTitle': 'Gizlilik ve Şifreleme', + 'settingsModal.ai.advancedEndpointOverride': 'Gelişmiş Taban URL Geçersiz Kılma', + 'settingsModal.ai.baseUrlHint': + "Sağlayıcı varsayılan uç noktasını kullanmak için boş bırakın. Kendi proxy/worker URL'niz için bunu kullanın.", + 'settingsModal.ai.resetEndpoint': 'Varsayılana sıfırla', + 'settingsModal.ai.customHeadersTitle': 'Özel Başlıklar', + 'settingsModal.ai.customHeadersSubtitle': + 'Cloudflare Access gibi kimlik doğrulama vekilleri için ekstra başlıklar gönderin.', + 'settingsModal.ai.customHeadersEmpty': 'Yapılandırılmış özel başlık yok.', + 'settingsModal.ai.customHeadersSecurity': + 'Başlık değerleri tarayıcı profilinizde yerel olarak saklanır.', + 'settingsModal.ai.addHeader': 'Başlık Ekle', + 'settingsModal.ai.cloudflarePreset': 'Cloudflare Ön Ayarını Kullan', + 'settingsModal.ai.customEndpoints.ollama.name': 'Ollama', + 'settingsModal.ai.customEndpoints.lmStudio.name': 'LM Studio', + 'settingsModal.ai.customEndpoints.together.name': 'Together.ai', + 'settingsModal.ai.risk.browserFriendly': 'Tarayıcı uyumlu', + 'settingsModal.ai.risk.proxyLikely': 'Muhtemelen vekil sunucu gerekli', + 'settingsModal.ai.risk.mixed': 'Uç noktasına bağlı', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.label': '2.5 Flash Lite', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.hint': + 'En hızlı · Ücretsiz katman varsayılanı', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.category': 'Hız', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.badge': 'Varsayılan', + 'settingsModal.ai.models.gemini.gemini-2.5-flash.label': '2.5 Flash', + 'settingsModal.ai.models.gemini.gemini-2.5-flash.hint': 'En iyi fiyat/performans dengesi', + 'settingsModal.ai.models.gemini.gemini-2.5-flash.category': 'Hız', + 'settingsModal.ai.models.gemini.gemini-2.5-pro.label': '2.5 Pro', + 'settingsModal.ai.models.gemini.gemini-2.5-pro.hint': + 'En iyi akıl yürütme · Karmaşık diyagramlar', + 'settingsModal.ai.models.gemini.gemini-2.5-pro.category': 'Akıl Yürütme', + 'settingsModal.ai.models.gemini.gemini-3-flash.label': '3 Flash', + 'settingsModal.ai.models.gemini.gemini-3-flash.hint': 'Sınır hızı + zeka', + 'settingsModal.ai.models.gemini.gemini-3-flash.category': 'Eski', + 'settingsModal.ai.models.gemini.gemini-3-flash.badge': 'Yeni', + 'settingsModal.ai.models.gemini.gemini-3-pro.label': '3 Pro', + 'settingsModal.ai.models.gemini.gemini-3-pro.hint': 'En güçlü · Çok modlu', + 'settingsModal.ai.models.gemini.gemini-3-pro.category': 'Eski', + 'settingsModal.ai.models.gemini.gemini-3-pro.badge': 'Yeni', + 'settingsModal.ai.models.openai.gpt-5-mini.label': 'GPT-5 mini', + 'settingsModal.ai.models.openai.gpt-5-mini.hint': 'Hızlı · Maliyet verimli', + 'settingsModal.ai.models.openai.gpt-5-mini.category': 'Hız', + 'settingsModal.ai.models.openai.gpt-5-mini.badge': 'Varsayılan', + 'settingsModal.ai.models.openai.gpt-5.label': 'GPT-5', + 'settingsModal.ai.models.openai.gpt-5.hint': 'Amiral gemisi · En yetenekli', + 'settingsModal.ai.models.openai.gpt-5.category': 'Amiral Gemisi', + 'settingsModal.ai.models.openai.gpt-5.2.label': 'GPT-5.2', + 'settingsModal.ai.models.openai.gpt-5.2.hint': 'Son güncelleme · Geliştirilmiş akıl yürütme', + 'settingsModal.ai.models.openai.gpt-5.2.category': 'Akıl Yürütme', + 'settingsModal.ai.models.openai.gpt-5.2.badge': 'Yeni', + 'settingsModal.ai.models.openai.o4-mini.label': 'o4-mini', + 'settingsModal.ai.models.openai.o4-mini.hint': 'Gelişmiş akıl yürütme · Hızlı', + 'settingsModal.ai.models.openai.o4-mini.category': 'Akıl Yürütme', + 'settingsModal.ai.models.openai.o4-mini.badge': 'Akıl Yürütme', + 'settingsModal.ai.models.openai.o3.label': 'o3', + 'settingsModal.ai.models.openai.o3.hint': 'Derin akıl yürütme · Karmaşık görevler', + 'settingsModal.ai.models.openai.o3.category': 'Akıl Yürütme', + 'settingsModal.ai.models.openai.o3.badge': 'Akıl Yürütme', + 'settingsModal.ai.models.claude.claude-haiku-4-5.label': 'Claude Haiku 4.5', + 'settingsModal.ai.models.claude.claude-haiku-4-5.hint': 'En hızlı · En uygun fiyatlı', + 'settingsModal.ai.models.claude.claude-haiku-4-5.category': 'Hız', + 'settingsModal.ai.models.claude.claude-sonnet-4-5.label': 'Claude Sonnet 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-5.hint': 'Dengeli zeka ve hız', + 'settingsModal.ai.models.claude.claude-sonnet-4-5.category': 'Amiral Gemisi', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.label': 'Claude Sonnet 4.6', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.hint': 'Son Sonnet · En iyi kodlama', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.category': 'Amiral Gemisi', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.badge': 'Varsayılan', + 'settingsModal.ai.models.claude.claude-opus-4-6.label': 'Claude Opus 4.6', + 'settingsModal.ai.models.claude.claude-opus-4-6.hint': 'En zeki · 1M jeton bağlamı', + 'settingsModal.ai.models.claude.claude-opus-4-6.category': 'Akıl Yürütme', + 'settingsModal.ai.models.claude.claude-opus-4-6.badge': 'Amiral Gemisi', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.hint': + 'Ücretsiz katman · Çok hızlı', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.category': 'Hız', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.badge': 'Ücretsiz', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.label': + 'Llama 4 Maverick', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.hint': + 'Daha yetenekli · Ücretsiz katman', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.category': 'Hız', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.badge': 'Ücretsiz', + 'settingsModal.ai.models.groq.qwen/qwen3-32b.label': 'Qwen3 32B', + 'settingsModal.ai.models.groq.qwen/qwen3-32b.hint': 'Gelişmiş akıl yürütme · Araç kullanımı', + 'settingsModal.ai.models.groq.qwen/qwen3-32b.category': 'Akıl Yürütme', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.label': 'Llama 3.3 70B Versatile', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.hint': 'Çok yönlü model', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.category': 'Performans', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.badge': 'Performans', + 'settingsModal.ai.models.nvidia.meta/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.nvidia.meta/llama-4-scout-17b-16e-instruct.hint': 'Verimli · Çok modlu', + 'settingsModal.ai.models.nvidia.meta/llama-4-scout-17b-16e-instruct.category': 'Hız', + 'settingsModal.ai.models.nvidia.nvidia/nemotron-nano-12b-v2-vl.label': 'Nemotron Nano 12B', + 'settingsModal.ai.models.nvidia.nvidia/nemotron-nano-12b-v2-vl.hint': + 'Hafif · Görsel-dil · Hızlı', + 'settingsModal.ai.models.nvidia.nvidia/nemotron-nano-12b-v2-vl.category': 'Hız', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.label': 'DeepSeek-V3.2 (685B)', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.hint': 'Son sürüm · GPT-5 seviyesinde', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.category': 'Amiral Gemisi', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.badge': 'Yeni', + 'settingsModal.ai.models.nvidia.qwen/qwq-32b.label': 'QwQ 32B', + 'settingsModal.ai.models.nvidia.qwen/qwq-32b.hint': 'Güçlü akıl yürütme modeli', + 'settingsModal.ai.models.nvidia.qwen/qwq-32b.category': 'Akıl Yürütme', + 'settingsModal.ai.models.nvidia.moonshotai/kimi-k2-thinking.label': 'Kimi K2 Thinking', + 'settingsModal.ai.models.nvidia.moonshotai/kimi-k2-thinking.hint': + 'Gelişmiş akıl yürütme · Araç kullanımı', + 'settingsModal.ai.models.nvidia.moonshotai/kimi-k2-thinking.category': 'Akıl Yürütme', + 'settingsModal.ai.models.cerebras.gpt-oss-120b.label': 'GPT-OSS 120B', + 'settingsModal.ai.models.cerebras.gpt-oss-120b.hint': "120B parametre · WSE-3'te hızlı", + 'settingsModal.ai.models.cerebras.gpt-oss-120b.category': 'Hız', + 'settingsModal.ai.models.cerebras.gpt-oss-120b.badge': 'Varsayılan', + 'settingsModal.ai.models.cerebras.qwen-3-32b.label': 'Qwen3 32B', + 'settingsModal.ai.models.cerebras.qwen-3-32b.hint': '2.403 jeton/sn · Endüstrinin en hızlısı', + 'settingsModal.ai.models.cerebras.qwen-3-32b.category': 'Hız', + 'settingsModal.ai.models.cerebras.qwen-3-32b.badge': 'En Hızlı', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.label': 'Qwen3 235B A22B', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.hint': 'Amiral gemisi · En iyi kalite', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.category': 'Amiral Gemisi', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.badge': 'Amiral Gemisi', + 'settingsModal.ai.models.cerebras.zai-glm-4.7.label': 'Zai-GLM 4.7', + 'settingsModal.ai.models.cerebras.zai-glm-4.7.hint': 'Gelişmiş akıl yürütme · Araç kullanımı', + 'settingsModal.ai.models.cerebras.zai-glm-4.7.category': 'Akıl Yürütme', + 'settingsModal.ai.models.mistral.mistral-small-latest.label': 'Mistral Small', + 'settingsModal.ai.models.mistral.mistral-small-latest.hint': + 'Hızlı · Maliyet verimli · 32k bağlam', + 'settingsModal.ai.models.mistral.mistral-small-latest.category': 'Hız', + 'settingsModal.ai.models.mistral.mistral-small-latest.badge': 'Ücretsiz', + 'settingsModal.ai.models.mistral.mistral-medium-latest.label': 'Mistral Medium', + 'settingsModal.ai.models.mistral.mistral-medium-latest.hint': + 'Dengeli kalite-maliyet · En iyi varsayılan', + 'settingsModal.ai.models.mistral.mistral-medium-latest.category': 'Amiral Gemisi', + 'settingsModal.ai.models.mistral.mistral-medium-latest.badge': 'Varsayılan', + 'settingsModal.ai.models.mistral.mistral-large-latest.label': 'Mistral Large', + 'settingsModal.ai.models.mistral.mistral-large-latest.hint': + 'En yetenekli · 128k bağlam · Amiral gemisi', + 'settingsModal.ai.models.mistral.mistral-large-latest.category': 'Amiral Gemisi', + 'settingsModal.ai.models.mistral.mistral-large-latest.badge': 'Amiral Gemisi', + 'settingsModal.ai.models.mistral.codestral-latest.label': 'Codestral', + 'settingsModal.ai.models.mistral.codestral-latest.hint': + 'Kod için optimize edilmiş · 256k bağlam', + 'settingsModal.ai.models.mistral.codestral-latest.category': 'Kodlama', + 'settingsModal.ai.models.mistral.codestral-latest.badge': 'Kod', + 'settingsModal.ai.models.mistral.pixtral-large-latest.label': 'Pixtral Large', + 'settingsModal.ai.models.mistral.pixtral-large-latest.hint': 'Görsel + akıl yürütme · Çok modlu', + 'settingsModal.ai.models.mistral.pixtral-large-latest.category': 'Çok Modlu', + 'settingsModal.ai.models.mistral.pixtral-large-latest.badge': 'Görsel', + 'settingsModal.ai.models.custom.custom.label': 'Özel Model', + 'settingsModal.ai.models.custom.custom.hint': 'Model kimliğinizi aşağıya girin', + 'settingsModal.ai.models.custom.custom.category': 'Özel', + 'settingsModal.ai.byok.dataPrivacy': 'Verileriniz asla sunucularımızdan geçmez', + 'settingsModal.ai.byok.control': 'Maliyet ve hız sınırları üzerinde tam kontrol', + 'settingsModal.ai.byok.flexibility': + 'Yeniden bağlamadan istediğiniz zaman sağlayıcıları değiştirin', + 'settingsModal.ai.byok.cuttingEdge': 'Son teknoloji modellere çıktıkları an erişin', + 'settingsModal.ai.customEndpoints.ollama.hint': 'Yerel · Ücretsiz', + 'settingsModal.ai.customEndpoints.lmStudio.hint': 'Yerel · Ücretsiz', + 'settingsModal.ai.customEndpoints.together.hint': 'Bulut · Hızlı', + 'settingsModal.brand.logo': 'Logo', + 'settingsModal.brand.favicon': 'Favicon', + 'settingsModal.brand.googleFontsHint': "Google Fonts'tan dinamik olarak yüklenir", + 'settingsModal.brand.cornerRadius': 'Köşe Yarıçapı', + 'settingsModal.brand.glassmorphism': 'Cam Efekti', + 'settingsModal.brand.beveledButtons': 'Kabartmalı Düğmeler', + 'settingsModal.brand.beveledButtonsHint': 'Düğmelere derinlik ve kenarlık ekler', + 'settingsModal.brand.showBetaBadge': 'Beta Rozetini Göster', + 'settingsModal.brand.showBetaBadgeHint': 'Logo yanında BETA çipini göster', + 'settingsModal.canvas.routingProfile': 'Yönlendirme Profili', + 'settingsModal.canvas.routingProfileStandard': 'Standart', + 'settingsModal.canvas.routingProfileInfrastructure': 'Altyapı', + 'settingsModal.canvas.routingProfileHint': + 'Altyapı modu servis haritaları için dikgen rotaları tercih eder.', + 'settingsModal.canvas.edgeBundling': 'Paralel Kenarları Birleştir', + 'settingsModal.canvas.edgeBundlingDesc': 'Paralel bağlantıları paylaşılan şeritlerde tut', + 'toolbar.flowpilot': 'Flowpilot', + 'toolbar.commandCenter': 'Komut Merkezini Aç', + 'flowEditor.dslExportSkippedEdges': 'DSL dışa aktarmada {{count}} geçersiz kenar atlandı.', + 'connectMenu.close': 'Bağlantı menüsünü kapat', + 'snapshotsPanel.close': 'Anlık görüntü panelini kapat', + 'connectionPanel.label': 'Etiket', + 'connectionPanel.route': 'Yol', + 'connectionPanel.appearance': 'Görünüm', + 'connectionPanel.style': 'Stil', + 'connectionPanel.actions': 'İşlemler', +}; + +// The script continues with de, fr, es, zh, ja translations... +// For now, let's apply translations for each language file + +const allTranslations = { tr }; + +const EXTRA_KEYS = { + tr: ['common.previous', 'common.settings', 'common.docs', 'common.publicBeta'], +}; + +for (const [lang, overrides] of Object.entries(allTranslations)) { + const filePath = path.join(LOCALES, lang, 'translation.json'); + const doc = readJson(filePath); + + // Delete extra keys + if (EXTRA_KEYS[lang]) { + for (const key of EXTRA_KEYS[lang]) { + deleteByPath(doc, key); + } + } + + // Apply translations + let count = 0; + for (const [key, value] of Object.entries(overrides)) { + const current = getByPath(doc, key); + if (typeof current === 'string') { + setByPath(doc, key, value); + count++; + } + } + + writeJson(filePath, doc); + console.log(`${lang}: applied ${count} translations`); +} diff --git a/scripts/translate-remaining.mjs b/scripts/translate-remaining.mjs new file mode 100644 index 0000000..c8342be --- /dev/null +++ b/scripts/translate-remaining.mjs @@ -0,0 +1,1147 @@ +import fs from 'fs'; +import path from 'path'; + +const locales = { + tr: JSON.parse(fs.readFileSync('src/i18n/locales/tr/translation.json')), + de: JSON.parse(fs.readFileSync('src/i18n/locales/de/translation.json')), + fr: JSON.parse(fs.readFileSync('src/i18n/locales/fr/translation.json')), + es: JSON.parse(fs.readFileSync('src/i18n/locales/es/translation.json')), + zh: JSON.parse(fs.readFileSync('src/i18n/locales/zh/translation.json')), + ja: JSON.parse(fs.readFileSync('src/i18n/locales/ja/translation.json')), +}; + +const translations = { + tr: { + 'nav.beta': 'BETA', + 'export.openflowdslLabel': '{{appName}} DSL', + 'landing.nav.figma': 'Figma', + 'home.continueTitle': 'Son eylemle devam et', + 'home.suggestionBlank': 'Boş tuval', + 'home.suggestionBlankDesc': 'Doğrudan editöre geç', + 'home.suggestionAI': 'Flowpilot AI', + 'home.suggestionAIDesc': 'Bir prompt ile başla', + 'home.suggestionImport': 'İçe Aktar', + 'home.suggestionImportDesc': 'Mevcut çalışmanı getir', + 'home.suggestionTemplates': 'Şablonlar', + 'home.suggestionTemplatesDesc': 'Kanıtlanmış bir kalıptan başla', + 'ai.model': 'Yapay Zeka Modeli', + 'history.undoUnavailable': 'Geri alınacak bir şey yok.', + 'history.redoUnavailable': 'İleri alınacak bir şey yok.', + 'welcome.title': 'OpenFlowKit', + 'welcome.shortcutsTitle': 'Kısayollarla hızlı başla', + 'welcome.shortcutsBadge': 'Klavye öncelikli', + 'welcome.shortcutCommandBar': 'Komut merkezini aç', + 'welcome.shortcutHelp': 'Klavye kısayollarını görüntüle', + 'welcome.shortcutCanvas': 'Tuvalde düğüm ekle', + 'cta.github': 'GitHub', + 'share.betaBadge': 'Beta', + 'chatbot.aiName': 'Yapay Zeka', + 'chatbot.prompts.keyboardShortcuts.icon': '⌘', + 'chatbot.prompts.nodeTypes.icon': '❖', + 'chatbot.prompts.dslSyntax.icon': '{}', + 'commandBar.figmaImport.fileUrlPlaceholder': 'https://www.figma.com/design/...', + 'commandBar.figmaImport.tokenPlaceholder': 'figd_...', + 'commandBar.import.categories.sql': 'SQL', + 'commandBar.import.categories.mindmap': 'Zihin Haritası', + 'commandBar.import.categories.markdown': 'Markdown', + 'canvas.addNodeShortcut': 'Düğüm ekle', + 'canvas.aiChatPlaceholder': 'Bir şeyler oluştur...', + 'canvas.nodes': 'Düğümler', + 'canvas.connections': 'Bağlantılar', + 'canvas.elements': 'Öğeler', + 'canvas.selection': 'Seçim', + 'canvas.noSelection': 'Seçim yok', + 'canvas.alignNodes': 'Düğümleri hizala', + 'canvas.distributeNodes': 'Düğümleri dağıt', + 'canvas.alignLeft': 'Sola hizala', + 'canvas.alignCenter': 'Merkeze hizala', + 'canvas.alignRight': 'Sağa hizala', + 'canvas.alignTop': 'Üste hizala', + 'canvas.alignMiddle': 'Ortaya hizala', + 'canvas.alignBottom': 'Alta hizala', + 'canvas.distributeHorizontally': 'Yatay dağıt', + 'canvas.distributeVertically': 'Dikey dağıt', + 'canvas.zoomToFit': 'Sığdırmak için yakınlaştır', + 'canvas.zoomToSelection': 'Seçime yakınlaştır', + 'canvas.locked': 'Kilitli', + 'canvas.unlock': 'Kilidi aç', + 'canvas.lock': 'Kilitle', + 'canvas.bringToFront': 'Öne getir', + 'canvas.sendToBack': 'Arkaya gönder', + 'canvas.group': 'Grupla', + 'canvas.ungroup': 'Grubu kaldır', + 'sidebar.searchPlaceholder': 'Ara...', + 'sidebar.noResults': 'Sonuç yok', + 'sidebar.nodes': 'Düğümler', + 'sidebar.components': 'Bileşenler', + 'sidebar.snippets': 'Parçacıklar', + 'sidebar.addons': 'Eklentiler', + 'mindmap.addChild': 'Alt konu ekle', + 'mindmap.addSibling': 'Kardeş konu ekle', + 'mindmap.addParent': 'Üst konu ekle', + 'mindmap.delete': 'Sil', + 'mindmap.insertAfter': 'Sonra ekle', + 'mindmap.insertBefore': 'Önce ekle', + 'aiModel.title': 'AI Modeli', + 'aiModel.provider': 'Sağlayıcı', + 'aiModel.model': 'Model', + 'aiModel.temperature': 'Sıcaklık', + 'aiModel.maxTokens': 'Maksimum belirteç', + 'aiModel.buttons.retry': 'Tekrar dene', + 'aiModel.buttons.stop': 'Durdur', + 'aiModel.buttons.clear': 'Temizle', + 'aiModel.thinking': 'Düşünüyor...', + 'aiModel.error': 'AI hatası', + 'share.title': 'Paylaş', + 'share.description': 'Bu tuvali paylaş', + 'share.copyLink': 'Bağlantıyı kopyala', + 'share.embed': 'Göm', + 'share.embedCode': 'Gömme kodu', + 'share.export': 'Dışa aktar', + 'share.settings': 'Paylaşım ayarları', + 'share.anyoneWithLink': 'Bağlantısı olan herkes', + 'share.viewOnly': 'Yalnızca görüntüleme', + 'share.canEdit': 'Düzenleyebilir', + 'share.disable': 'Paylaşımı kapat', + 'share.enable': 'Paylaşımı aç', + 'share.invite': 'Davet et', + 'share.pending': 'Beklemede', + 'share.revoke': 'İptal et', + 'share.expired': 'Süresi doldu', + 'settingsModal.title': 'Ayarlar', + 'settingsModal.search': 'Ara...', + 'settingsModal.general': 'Genel', + 'settingsModal.appearance': 'Görünüm', + 'settingsModal.canvas': 'Tuval', + 'settingsModal.shortcuts': 'Kısayollar', + 'settingsModal.account': 'Hesap', + 'settingsModal.about': 'Hakkında', + 'settingsModal.language': 'Dil', + 'settingsModal.theme': 'Tema', + 'settingsModal.fontSize': 'Yazı boyutu', + 'settingsModal.fontFamily': 'Yazı tipi', + 'settingsModal.save': 'Kaydet', + 'settingsModal.cancel': 'İptal', + 'settingsModal.reset': 'Sıfırla', + 'settingsModal.confirm': 'Onayla', + 'settingsModal.ai.model': 'Yapay Zeka Modeli', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.label': 'Gemini 2.5 Flash Lite', + 'settingsModal.ai.models.gemini.gemini-2.5-flash.label': 'Gemini 2.5 Flash', + 'settingsModal.ai.models.gemini.gemini-2.5-pro.label': 'Gemini 2.5 Pro', + 'settingsModal.ai.models.gemini.gemini-3-flash.label': 'Gemini 3 Flash', + 'settingsModal.ai.models.gemini.gemini-3-pro.label': 'Gemini 3 Pro', + 'settingsModal.ai.models.openai.gpt-5-mini.label': 'GPT-5 Mini', + 'settingsModal.ai.models.openai.gpt-5.label': 'GPT-5', + 'settingsModal.ai.models.openai.gpt-5.2.label': 'GPT-5.2', + 'settingsModal.ai.models.openai.o4-mini.label': 'O4-Mini', + 'settingsModal.ai.models.openai.o3.label': 'O3', + 'settingsModal.ai.models.claude.claude-haiku-4-5.label': 'Claude Haiku 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-5.label': 'Claude Sonnet 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.label': 'Claude Sonnet 4.6', + 'settingsModal.ai.models.claude.claude-opus-4-6.label': 'Claude Opus 4.6', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.label': + 'Llama 4 Maverick', + 'settingsModal.ai.models.groq.qwen/qwen3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.label': 'Llama 3.3 70B', + 'settingsModal.ai.models.nvidia.meta/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.nvidia.nvidia/nemotron-nano-12b-v2-vl.label': 'Nemotron Nano 12B', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.label': 'DeepSeek V3-2', + 'settingsModal.ai.models.nvidia.qwen/qwq-32b.label': 'QwQ 32B', + 'settingsModal.ai.models.nvidia.moonshotai/kimi-k2-thinking.label': 'Kimi K2 Thinking', + 'settingsModal.ai.models.cerebras.gpt-oss-120b.label': 'GPT OSS 120B', + 'settingsModal.ai.models.cerebras.qwen-3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.label': 'Qwen 3 235B', + 'settingsModal.ai.models.cerebras.zai-glm-4.7.label': 'Zai GLM 4.7', + 'settingsModal.ai.models.mistral.mistral-small-latest.label': 'Mistral Small', + 'settingsModal.ai.models.mistral.mistral-medium-latest.label': 'Mistral Medium', + 'settingsModal.ai.models.mistral.mistral-large-latest.label': 'Mistral Large', + 'settingsModal.ai.models.mistral.codestral-latest.label': 'Codestral', + 'settingsModal.ai.models.mistral.pixtral-large-latest.label': 'Pixtral Large', + 'settingsModal.ai.customEndpoints.ollama.name': 'Ollama', + 'settingsModal.ai.customEndpoints.lmStudio.name': 'LM Studio', + 'settingsModal.ai.customEndpoints.together.name': 'Together AI', + 'settingsModal.brand.logo': 'Logo', + 'settingsModal.brand.favicon': 'Favicon', + 'toolbar.flowpilot': 'Flowpilot', + 'contextMenu.label': 'Tuval Bağlam Menüsü', + 'connectMenu.label': 'Düğüm Bağlantı Menüsü', + 'home.homeEmptyTitle': 'İlk akışınızı oluşturun', + 'home.homeEmptySubtitle': + 'Kurumsal düzey mimariileri anında tasarlayın. Boş bir tuvalden başlayın, altyapınızı AI builderımızla tanımlayın veya hazır bir şablon kullanın.', + 'home.homeBlankCanvas': 'Boş Tuval', + 'home.homeFlowpilotAI': 'Flowpilot Yapay Zeka', + 'home.homeTemplates': 'Şablonlar', + 'home.homeImportFile': 'Veya mevcut bir dosyayı içe aktarın', + 'commandBar.import.categories.openapi': 'OpenAPI', + 'commandBar.import.infraFormats.terraformState': 'Terraform Durumu', + 'commandBar.import.infraFormats.kubernetes': 'Kubernetes', + 'commandBar.import.infraFormats.dockerCompose': 'Docker Compose', + 'commandBar.layout.normal': 'Normal', + 'commandBar.visuals.bezier': 'Eğri', + 'commandBar.visuals.largeGraphSafetyOn': 'Büyük Grafik Güvenliği Açık', + 'commandBar.code.quickFixes': 'Hızlı düzeltmeler', + 'commandBar.code.linePrefix': 'Satır {{line}}: ', + 'commandBar.code.hintPrefix': 'İpucu:', + 'commandBar.code.strictModeGuidance.defineEndpoints': + 'Bağlantıları yapmadan önce her kenar uç noktasını açık bir mimari düğüm olarak tanımlayın.', + 'commandBar.code.strictModeGuidance.uniqueIds': + 'Her servis/grup/düğüm için benzersiz kimlikler kullanın (yinelenen mimari kimlikler yok).', + 'commandBar.code.strictModeGuidance.edgeSyntax': + 'Mimari kenar okları `-->`, `<--`, veya `<-->` ve api:R --> L:db gibi taraf niteleyicilerini kullanın.', + 'commandBar.code.strictModeGuidance.nodeSyntax': + 'Geçerli düğüm bildirimleri kullanın: `service id(icon)[Label]`, `group id[Label]`, `junction id[Label]`.', + 'commandBar.code.strictModeGuidance.fallback': + 'Otomatik kurtarmaya izin vermek için Mimari Katı Modunu kapatın veya tanılamaları düzeltip tekrar deneyin.', + 'settingsModal.canvas.architectureStrictMode': 'Mimari Katı Modu', + 'settingsModal.canvas.architectureStrictModeDesc': + 'Mimari tanılamalar kurtarma/doğrulama sorunları içerdiğinde Mermaid içe aktarmayı engelle', + 'flowCanvas.strictModePasteBlocked': + 'Mimari katı modu Mermaid yapıştırmasını engelledi. Kod görünümünü açın, tanılamaları düzeltin, sonra tekrar deneyin.', + }, + de: { + 'nav.beta': 'BETA', + 'export.openflowdslLabel': '{{appName}} DSL', + 'landing.nav.figma': 'Figma', + 'home.continueTitle': 'Mit einer kürzlichen Aktion fortfahren', + 'home.suggestionBlank': 'Leere Leinwand', + 'home.suggestionBlankDesc': 'Direkt in den Editor einsteigen', + 'home.suggestionAI': 'Flowpilot KI', + 'home.suggestionAIDesc': 'Mit einer Eingabe beginnen', + 'home.suggestionImport': 'Importieren', + 'home.suggestionImportDesc': 'Bestehende Arbeit einbringen', + 'home.suggestionTemplates': 'Vorlagen', + 'home.suggestionTemplatesDesc': 'Mit einem bewährten Muster beginnen', + 'ai.model': 'Modell', + 'history.undoUnavailable': 'Noch nichts rückgängig zu machen.', + 'history.redoUnavailable': 'Im Moment nichts zu wiederholen.', + 'welcome.title': 'OpenFlowKit', + 'welcome.shortcutsTitle': 'Schnellstart mit Tastenkürzeln', + 'welcome.shortcutsBadge': 'Tastatur zuerst', + 'welcome.shortcutCommandBar': 'Befehlszentrale öffnen', + 'welcome.shortcutHelp': 'Tastenkürzel anzeigen', + 'welcome.shortcutCanvas': 'Knoten auf Leinwand hinzufügen', + 'cta.github': 'GitHub', + 'share.betaBadge': 'Beta', + 'chatbot.aiName': 'KI', + 'chatbot.prompts.keyboardShortcuts.icon': '⌘', + 'chatbot.prompts.nodeTypes.icon': '❖', + 'chatbot.prompts.dslSyntax.icon': '{}', + 'commandBar.figmaImport.fileUrlPlaceholder': 'https://www.figma.com/design/...', + 'commandBar.figmaImport.tokenPlaceholder': 'figd_...', + 'commandBar.import.categories.sql': 'SQL', + 'commandBar.import.categories.mindmap': 'Mindmap', + 'commandBar.import.categories.markdown': 'Markdown', + 'canvas.addNodeShortcut': 'Knoten hinzufügen', + 'canvas.aiChatPlaceholder': 'Etwas erstellen...', + 'canvas.nodes': 'Knoten', + 'canvas.connections': 'Verbindungen', + 'canvas.elements': 'Elemente', + 'canvas.selection': 'Auswahl', + 'canvas.noSelection': 'Keine Auswahl', + 'canvas.alignNodes': 'Knoten ausrichten', + 'canvas.distributeNodes': 'Knoten verteilen', + 'canvas.alignLeft': 'Links ausrichten', + 'canvas.alignCenter': 'Zentriert ausrichten', + 'canvas.alignRight': 'Rechts ausrichten', + 'canvas.alignTop': 'Oben ausrichten', + 'canvas.alignMiddle': 'Mitte ausrichten', + 'canvas.alignBottom': 'Unten ausrichten', + 'canvas.distributeHorizontally': 'Horizontal verteilen', + 'canvas.distributeVertically': 'Vertikal verteilen', + 'canvas.zoomToFit': 'Einpassen zoom', + 'canvas.zoomToSelection': 'Auf Auswahl zoomen', + 'canvas.locked': 'Gesperrt', + 'canvas.unlock': 'Entsperren', + 'canvas.lock': 'Sperren', + 'canvas.bringToFront': 'Nach vorne bringen', + 'canvas.sendToBack': 'Nach hinten senden', + 'canvas.group': 'Gruppieren', + 'canvas.ungroup': 'Gruppierung aufheben', + 'sidebar.searchPlaceholder': 'Suchen...', + 'sidebar.noResults': 'Keine Ergebnisse', + 'sidebar.nodes': 'Knoten', + 'sidebar.components': 'Komponenten', + 'sidebar.snippets': 'Snippets', + 'sidebar.addons': 'Add-ons', + 'mindmap.addChild': 'Unterthema hinzufügen', + 'mindmap.addSibling': 'Geschwisterthema hinzufügen', + 'mindmap.addParent': 'Übergeordnetes Thema hinzufügen', + 'mindmap.delete': 'Löschen', + 'mindmap.insertAfter': 'Danach einfügen', + 'mindmap.insertBefore': 'Davor einfügen', + 'aiModel.title': 'KI-Modell', + 'aiModel.provider': 'Anbieter', + 'aiModel.model': 'Modell', + 'aiModel.temperature': 'Temperatur', + 'aiModel.maxTokens': 'Maximale Token', + 'aiModel.buttons.retry': 'Erneut versuchen', + 'aiModel.buttons.stop': 'Stopp', + 'aiModel.buttons.clear': 'Löschen', + 'aiModel.thinking': 'Denkt nach...', + 'aiModel.error': 'KI-Fehler', + 'share.title': 'Teilen', + 'share.description': 'Diese Leinwand teilen', + 'share.copyLink': 'Link kopieren', + 'share.embed': 'Einbetten', + 'share.embedCode': 'Einbettungscode', + 'share.export': 'Exportieren', + 'share.settings': 'Freigabeeinstellungen', + 'share.anyoneWithLink': 'Jeder mit Link', + 'share.viewOnly': 'Nur ansehen', + 'share.canEdit': 'Kann bearbeiten', + 'share.disable': 'Freigabe deaktivieren', + 'share.enable': 'Freigabe aktivieren', + 'share.invite': 'Einladen', + 'share.pending': 'Ausstehend', + 'share.revoke': 'Widerrufen', + 'share.expired': 'Abgelaufen', + 'settingsModal.title': 'Einstellungen', + 'settingsModal.search': 'Suchen...', + 'settingsModal.general': 'Allgemein', + 'settingsModal.appearance': 'Erscheinungsbild', + 'settingsModal.canvas': 'Leinwand', + 'settingsModal.shortcuts': 'Tastenkürzel', + 'settingsModal.account': 'Konto', + 'settingsModal.about': 'Über', + 'settingsModal.language': 'Sprache', + 'settingsModal.theme': 'Design', + 'settingsModal.fontSize': 'Schriftgröße', + 'settingsModal.fontFamily': 'Schriftart', + 'settingsModal.save': 'Speichern', + 'settingsModal.cancel': 'Abbrechen', + 'settingsModal.reset': 'Zurücksetzen', + 'settingsModal.confirm': 'Bestätigen', + 'settingsModal.ai.model': 'KI-Modell', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.label': 'Gemini 2.5 Flash Lite', + 'settingsModal.ai.models.gemini.gemini-2.5-flash.label': 'Gemini 2.5 Flash', + 'settingsModal.ai.models.gemini.gemini-2.5-pro.label': 'Gemini 2.5 Pro', + 'settingsModal.ai.models.gemini.gemini-3-flash.label': 'Gemini 3 Flash', + 'settingsModal.ai.models.gemini.gemini-3-pro.label': 'Gemini 3 Pro', + 'settingsModal.ai.models.openai.gpt-5-mini.label': 'GPT-5 Mini', + 'settingsModal.ai.models.openai.gpt-5.label': 'GPT-5', + 'settingsModal.ai.models.openai.gpt-5.2.label': 'GPT-5.2', + 'settingsModal.ai.models.openai.o4-mini.label': 'O4-Mini', + 'settingsModal.ai.models.openai.o3.label': 'O3', + 'settingsModal.ai.models.claude.claude-haiku-4-5.label': 'Claude Haiku 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-5.label': 'Claude Sonnet 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.label': 'Claude Sonnet 4.6', + 'settingsModal.ai.models.claude.claude-opus-4-6.label': 'Claude Opus 4.6', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.label': + 'Llama 4 Maverick', + 'settingsModal.ai.models.groq.qwen/qwen3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.label': 'Llama 3.3 70B', + 'settingsModal.ai.models.nvidia.meta/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.nvidia.nvidia/nemotron-nano-12b-v2-vl.label': 'Nemotron Nano 12B', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.label': 'DeepSeek V3-2', + 'settingsModal.ai.models.nvidia.qwen/qwq-32b.label': 'QwQ 32B', + 'settingsModal.ai.models.nvidia.moonshotai/kimi-k2-thinking.label': 'Kimi K2 Thinking', + 'settingsModal.ai.models.cerebras.gpt-oss-120b.label': 'GPT OSS 120B', + 'settingsModal.ai.models.cerebras.qwen-3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.label': 'Qwen 3 235B', + 'settingsModal.ai.models.cerebras.zai-glm-4.7.label': 'Zai GLM 4.7', + 'settingsModal.ai.models.mistral.mistral-small-latest.label': 'Mistral Small', + 'settingsModal.ai.models.mistral.mistral-medium-latest.label': 'Mistral Medium', + 'settingsModal.ai.models.mistral.mistral-large-latest.label': 'Mistral Large', + 'settingsModal.ai.models.mistral.codestral-latest.label': 'Codestral', + 'settingsModal.ai.models.mistral.pixtral-large-latest.label': 'Pixtral Large', + 'settingsModal.ai.customEndpoints.ollama.name': 'Ollama', + 'settingsModal.ai.customEndpoints.lmStudio.name': 'LM Studio', + 'settingsModal.ai.customEndpoints.together.name': 'Together AI', + 'settingsModal.brand.logo': 'Logo', + 'settingsModal.brand.favicon': 'Favicon', + 'toolbar.flowpilot': 'Flowpilot', + 'contextMenu.label': 'Canvas-Kontextmenü', + 'connectMenu.label': 'Knotenverbindungsmenü', + 'home.homeEmptyTitle': 'Erstellen Sie Ihren ersten Flow', + 'home.homeEmptySubtitle': + 'Entwerfen Sie sofort Enterprise-Architekturen. Beginnen Sie mit einer leeren Leinwand, beschreiben Sie Ihre Infrastruktur mit unserem KI-Builder oder verwenden Sie eine maßgeschneiderte Vorlage.', + 'home.homeBlankCanvas': 'Leere Leinwand', + 'home.homeFlowpilotAI': 'Flowpilot KI', + 'home.homeTemplates': 'Vorlagen', + 'home.homeImportFile': 'Oder eine bestehende Datei importieren', + 'commandBar.import.categories.openapi': 'OpenAPI', + 'commandBar.import.infraFormats.terraformState': 'Terraform-Zustand', + 'commandBar.import.infraFormats.kubernetes': 'Kubernetes', + 'commandBar.import.infraFormats.dockerCompose': 'Docker Compose', + 'commandBar.layout.normal': 'Normal', + 'commandBar.visuals.bezier': 'Bezier', + 'commandBar.visuals.largeGraphSafetyOn': 'Großgraph-Sicherheit An', + 'commandBar.code.quickFixes': 'Schnellkorrekturen', + 'commandBar.code.linePrefix': 'Zeile {{line}}: ', + 'commandBar.code.hintPrefix': 'Hinweis:', + 'commandBar.code.strictModeGuidance.defineEndpoints': + 'Definieren Sie jeden Kantenendpunkt als expliziten Architekturknoten, bevor Sie Kanten verbinden.', + 'commandBar.code.strictModeGuidance.uniqueIds': + 'Verwenden Sie eindeutige IDs für jeden Service/Gruppe/Knoten (keine doppelten Architektur-IDs).', + 'commandBar.code.strictModeGuidance.edgeSyntax': + 'Verwenden Sie Architekturpfeile `-->` , `<--`, oder `<-->` und Seitend qualifizierer wie `api:R --> L:db`.', + 'commandBar.code.strictModeGuidance.nodeSyntax': + 'Verwenden Sie gültige Knotendeklarationen: `service id(icon)[Label]`, `group id[Label]`, `junction id[Label]`.', + 'commandBar.code.strictModeGuidance.fallback': + 'Deaktivieren Sie den Architektur-Strict-Modus, um eine automatische Wiederherstellung zu ermöglichen, oder beheben Sie die Diagnosen und versuchen Sie es erneut.', + 'settingsModal.canvas.architectureStrictMode': 'Architektur-Strict-Modus', + 'settingsModal.canvas.architectureStrictModeDesc': + 'Mermaid-Import blockieren, wenn Architekturdiagnosen Wiederherstellungs-/Validierungsprobleme enthalten', + 'flowCanvas.strictModePasteBlocked': + 'Architektur-Strict-Modus hat Mermaid-Einfügung blockiert. Code-Ansicht öffnen, Diagnosen beheben, dann erneut versuchen.', + }, + fr: { + 'nav.beta': 'BETA', + 'export.openflowdslLabel': '{{appName}} DSL', + 'landing.nav.figma': 'Figma', + 'home.continueTitle': 'Continuer avec une action récente', + 'home.suggestionBlank': 'Canevas vierge', + 'home.suggestionBlankDesc': "aller directement dans l'éditeur", + 'home.suggestionAI': 'Flowpilot IA', + 'home.suggestionAIDesc': "Commencer à partir d'une invite", + 'home.suggestionImport': 'Importer', + 'home.suggestionImportDesc': 'Importer un travail existant', + 'home.suggestionTemplates': 'Modèles', + 'home.suggestionTemplatesDesc': "Commencer à partir d'un modèle éprouvé", + 'ai.model': 'Modèle', + 'history.undoUnavailable': 'Rien à annuler pour le moment.', + 'history.redoUnavailable': 'Rien à refaire pour le moment.', + 'welcome.title': 'OpenFlowKit', + 'welcome.shortcutsTitle': 'Démarrez rapidement avec les raccourcis', + 'welcome.shortcutsBadge': "Clavier d'abord", + 'welcome.shortcutCommandBar': 'Ouvrir le centre de commandes', + 'welcome.shortcutHelp': 'Voir les raccourcis clavier', + 'welcome.shortcutCanvas': 'Ajouter un nœud sur le canevas', + 'cta.github': 'GitHub', + 'share.betaBadge': 'Bêta', + 'chatbot.aiName': 'IA', + 'chatbot.prompts.keyboardShortcuts.icon': '⌘', + 'chatbot.prompts.nodeTypes.icon': '❖', + 'chatbot.prompts.dslSyntax.icon': '{}', + 'commandBar.figmaImport.fileUrlPlaceholder': 'https://www.figma.com/design/...', + 'commandBar.figmaImport.tokenPlaceholder': 'figd_...', + 'commandBar.import.categories.sql': 'SQL', + 'commandBar.import.categories.mindmap': 'Carte mentale', + 'commandBar.import.categories.markdown': 'Markdown', + 'canvas.addNodeShortcut': 'Ajouter un nœud', + 'canvas.aiChatPlaceholder': 'Créer quelque chose...', + 'canvas.nodes': 'Nœuds', + 'canvas.connections': 'Connexions', + 'canvas.elements': 'Éléments', + 'canvas.selection': 'Sélection', + 'canvas.noSelection': 'Aucune sélection', + 'canvas.alignNodes': 'Aligner les nœuds', + 'canvas.distributeNodes': 'Distribuer les nœuds', + 'canvas.alignLeft': 'Aligner à gauche', + 'canvas.alignCenter': 'Aligner au centre', + 'canvas.alignRight': 'Aligner à droite', + 'canvas.alignTop': 'Aligner en haut', + 'canvas.alignMiddle': 'Aligner au milieu', + 'canvas.alignBottom': 'Aligner en bas', + 'canvas.distributeHorizontally': 'Distribuer horizontalement', + 'canvas.distributeVertically': 'Distribuer verticalement', + 'canvas.zoomToFit': 'Zoom pour ajuster', + 'canvas.zoomToSelection': 'Zoom sur la sélection', + 'canvas.locked': 'Verrouillé', + 'canvas.unlock': 'Déverrouiller', + 'canvas.lock': 'Verrouiller', + 'canvas.bringToFront': 'Mettre au premier plan', + 'canvas.sendToBack': "Envoyer à l'arrière", + 'canvas.group': 'Grouper', + 'canvas.ungroup': 'Dissocier', + 'sidebar.searchPlaceholder': 'Rechercher...', + 'sidebar.noResults': 'Aucun résultat', + 'sidebar.nodes': 'Nœuds', + 'sidebar.components': 'Composants', + 'sidebar.snippets': 'Extraits', + 'sidebar.addons': 'Add-ons', + 'mindmap.addChild': 'Ajouter un sous-sujet', + 'mindmap.addSibling': 'Ajouter un sujet frère', + 'mindmap.addParent': 'Ajouter un sujet parent', + 'mindmap.delete': 'Supprimer', + 'mindmap.insertAfter': 'Insérer après', + 'mindmap.insertBefore': 'Insérer avant', + 'aiModel.title': 'Modèle IA', + 'aiModel.provider': 'Fournisseur', + 'aiModel.model': 'Modèle', + 'aiModel.temperature': 'Température', + 'aiModel.maxTokens': 'Jetons max', + 'aiModel.buttons.retry': 'Réessayer', + 'aiModel.buttons.stop': 'Arrêter', + 'aiModel.buttons.clear': 'Effacer', + 'aiModel.thinking': 'Réflexion...', + 'aiModel.error': 'Erreur IA', + 'share.title': 'Partager', + 'share.description': 'Partager ce canevas', + 'share.copyLink': 'Copier le lien', + 'share.embed': 'Intégrer', + 'share.embedCode': "Code d'intégration", + 'share.export': 'Exporter', + 'share.settings': 'Paramètres de partage', + 'share.anyoneWithLink': 'Toute personne avec le lien', + 'share.viewOnly': 'Lecture seule', + 'share.canEdit': 'Peut modifier', + 'share.disable': 'Désactiver le partage', + 'share.enable': 'Activer le partage', + 'share.invite': 'Inviter', + 'share.pending': 'En attente', + 'share.revoke': 'Révoquer', + 'share.expired': 'Expiré', + 'settingsModal.title': 'Paramètres', + 'settingsModal.search': 'Rechercher...', + 'settingsModal.general': 'Général', + 'settingsModal.appearance': 'Apparence', + 'settingsModal.canvas': 'Canevas', + 'settingsModal.shortcuts': 'Raccourcis', + 'settingsModal.account': 'Compte', + 'settingsModal.about': 'À propos', + 'settingsModal.language': 'Langue', + 'settingsModal.theme': 'Thème', + 'settingsModal.fontSize': 'Taille de police', + 'settingsModal.fontFamily': 'Police', + 'settingsModal.save': 'Enregistrer', + 'settingsModal.cancel': 'Annuler', + 'settingsModal.reset': 'Réinitialiser', + 'settingsModal.confirm': 'Confirmer', + 'settingsModal.ai.model': 'Modèle IA', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.label': 'Gemini 2.5 Flash Lite', + 'settingsModal.ai.models.gemini.gemini-2.5-flash.label': 'Gemini 2.5 Flash', + 'settingsModal.ai.models.gemini.gemini-2.5-pro.label': 'Gemini 2.5 Pro', + 'settingsModal.ai.models.gemini.gemini-3-flash.label': 'Gemini 3 Flash', + 'settingsModal.ai.models.gemini.gemini-3-pro.label': 'Gemini 3 Pro', + 'settingsModal.ai.models.openai.gpt-5-mini.label': 'GPT-5 Mini', + 'settingsModal.ai.models.openai.gpt-5.label': 'GPT-5', + 'settingsModal.ai.models.openai.gpt-5.2.label': 'GPT-5.2', + 'settingsModal.ai.models.openai.o4-mini.label': 'O4-Mini', + 'settingsModal.ai.models.openai.o3.label': 'O3', + 'settingsModal.ai.models.claude.claude-haiku-4-5.label': 'Claude Haiku 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-5.label': 'Claude Sonnet 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.label': 'Claude Sonnet 4.6', + 'settingsModal.ai.models.claude.claude-opus-4-6.label': 'Claude Opus 4.6', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.label': + 'Llama 4 Maverick', + 'settingsModal.ai.models.groq.qwen/qwen3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.label': 'Llama 3.3 70B', + 'settingsModal.ai.models.nvidia.meta/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.nvidia.nvidia/nemotron-nano-12b-v2-vl.label': 'Nemotron Nano 12B', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.label': 'DeepSeek V3-2', + 'settingsModal.ai.models.nvidia.qwen/qwq-32b.label': 'QwQ 32B', + 'settingsModal.ai.models.nvidia.moonshotai/kimi-k2-thinking.label': 'Kimi K2 Thinking', + 'settingsModal.ai.models.cerebras.gpt-oss-120b.label': 'GPT OSS 120B', + 'settingsModal.ai.models.cerebras.qwen-3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.label': 'Qwen 3 235B', + 'settingsModal.ai.models.cerebras.zai-glm-4.7.label': 'Zai GLM 4.7', + 'settingsModal.ai.models.mistral.mistral-small-latest.label': 'Mistral Small', + 'settingsModal.ai.models.mistral.mistral-medium-latest.label': 'Mistral Medium', + 'settingsModal.ai.models.mistral.mistral-large-latest.label': 'Mistral Large', + 'settingsModal.ai.models.mistral.codestral-latest.label': 'Codestral', + 'settingsModal.ai.models.mistral.pixtral-large-latest.label': 'Pixtral Large', + 'settingsModal.ai.customEndpoints.ollama.name': 'Ollama', + 'settingsModal.ai.customEndpoints.lmStudio.name': 'LM Studio', + 'settingsModal.ai.customEndpoints.together.name': 'Together AI', + 'settingsModal.brand.logo': 'Logo', + 'settingsModal.brand.favicon': 'Favicon', + 'toolbar.flowpilot': 'Flowpilot', + 'contextMenu.label': 'Menu contextuel du canevas', + 'connectMenu.label': 'Menu de connexion des nœuds', + 'home.homeEmptyTitle': 'Créez votre premier flux', + 'home.homeEmptySubtitle': + 'Concevez des architectures de qualité entreprise instantanément. Commencez avec un canevas vierge, décrivez votre infrastructure avec notre constructeur IA, ou utilisez un modèle adapté.', + 'home.homeBlankCanvas': 'Canevas vierge', + 'home.homeFlowpilotAI': 'Flowpilot IA', + 'home.homeTemplates': 'Modèles', + 'home.homeImportFile': 'Ou importer un fichier existant', + 'commandBar.import.categories.openapi': 'OpenAPI', + 'commandBar.import.infraFormats.terraformState': 'État Terraform', + 'commandBar.import.infraFormats.kubernetes': 'Kubernetes', + 'commandBar.import.infraFormats.dockerCompose': 'Docker Compose', + 'commandBar.layout.normal': 'Normal', + 'commandBar.visuals.bezier': 'Bezier', + 'commandBar.visuals.largeGraphSafetyOn': 'Sécurité grand graphique On', + 'commandBar.code.quickFixes': 'Corrections rapides', + 'commandBar.code.linePrefix': 'Ligne {{line}}: ', + 'commandBar.code.hintPrefix': 'Indice:', + 'commandBar.code.strictModeGuidance.defineEndpoints': + "Définissez chaque point de terminaison de bord comme un nœud d'architecture explicite avant de connecter les bords.", + 'commandBar.code.strictModeGuidance.uniqueIds': + "Utilisez des ID uniques pour chaque service/groupe/nœud (pas d'ID d'architecture en double).", + 'commandBar.code.strictModeGuidance.edgeSyntax': + "Utilisez des flèches de bord d'architecture `-->` , `<--`, ou `<-->` et des qualificatifs de côté comme `api:R --> L:db`.", + 'commandBar.code.strictModeGuidance.nodeSyntax': + 'Utilisez des déclarations de nœuds valides: `service id(icon)[Label]`, `group id[Label]`, `junction id[Label]`.', + 'commandBar.code.strictModeGuidance.fallback': + "Désactivez le mode strict d'architecture pour permettre la récupération automatique, ou corrigez les diagnostics et réessayez.", + 'settingsModal.canvas.architectureStrictMode': 'Mode Strict Architecture', + 'settingsModal.canvas.architectureStrictModeDesc': + "Bloquer l'importation Mermaid lorsque les diagnostics d'architecture incluent des problèmes de récupération/validation", + 'flowCanvas.strictModePasteBlocked': + "Le mode strict d'architecture a bloqué le collage Mermaid. Ouvrez la vue Code, corrigez les diagnostics, puis réessayez.", + }, + es: { + 'nav.beta': 'BETA', + 'export.openflowdslLabel': '{{appName}} DSL', + 'landing.nav.figma': 'Figma', + 'home.continueTitle': 'Continuar con una acción reciente', + 'home.suggestionBlank': 'Lienzo en blanco', + 'home.suggestionBlankDesc': 'Saltar directamente al editor', + 'home.suggestionAI': 'Flowpilot IA', + 'home.suggestionAIDesc': 'Empezar desde un mensaje', + 'home.suggestionImport': 'Importar', + 'home.suggestionImportDesc': 'Traer trabajo existente', + 'home.suggestionTemplates': 'Plantillas', + 'home.suggestionTemplatesDesc': 'Empezar desde un patrón probado', + 'ai.model': 'Modelo', + 'history.undoUnavailable': 'Nada que deshacer aún.', + 'history.redoUnavailable': 'Nada que rehacer ahora.', + 'welcome.title': 'OpenFlowKit', + 'welcome.shortcutsTitle': 'Comienza rápido con atajos', + 'welcome.shortcutsBadge': 'Primero el teclado', + 'welcome.shortcutCommandBar': 'Abrir centro de comandos', + 'welcome.shortcutHelp': 'Ver atajos de teclado', + 'welcome.shortcutCanvas': 'Agregar nodo en lienzo', + 'cta.github': 'GitHub', + 'share.betaBadge': 'Beta', + 'chatbot.aiName': 'IA', + 'chatbot.prompts.keyboardShortcuts.icon': '⌘', + 'chatbot.prompts.nodeTypes.icon': '❖', + 'chatbot.prompts.dslSyntax.icon': '{}', + 'commandBar.figmaImport.fileUrlPlaceholder': 'https://www.figma.com/design/...', + 'commandBar.figmaImport.tokenPlaceholder': 'figd_...', + 'commandBar.import.categories.sql': 'SQL', + 'commandBar.import.categories.mindmap': 'Mapa mental', + 'commandBar.import.categories.markdown': 'Markdown', + 'canvas.addNodeShortcut': 'Agregar nodo', + 'canvas.aiChatPlaceholder': 'Crear algo...', + 'canvas.nodes': 'Nodos', + 'canvas.connections': 'Conexiones', + 'canvas.elements': 'Elementos', + 'canvas.selection': 'Selección', + 'canvas.noSelection': 'Sin selección', + 'canvas.alignNodes': 'Alinear nodos', + 'canvas.distributeNodes': 'Distribuir nodos', + 'canvas.alignLeft': 'Alinear a la izquierda', + 'canvas.alignCenter': 'Alinear al centro', + 'canvas.alignRight': 'Alinear a la derecha', + 'canvas.alignTop': 'Alinear arriba', + 'canvas.alignMiddle': 'Alinear al medio', + 'canvas.alignBottom': 'Alinear abajo', + 'canvas.distributeHorizontally': 'Distribuir horizontalmente', + 'canvas.distributeVertically': 'Distribuir verticalmente', + 'canvas.zoomToFit': 'Ajustar zoom', + 'canvas.zoomToSelection': 'Zoom a selección', + 'canvas.locked': 'Bloqueado', + 'canvas.unlock': 'Desbloquear', + 'canvas.lock': 'Bloquear', + 'canvas.bringToFront': 'Traer al frente', + 'canvas.sendToBack': 'Enviar atrás', + 'canvas.group': 'Agrupar', + 'canvas.ungroup': 'Desagrupar', + 'sidebar.searchPlaceholder': 'Buscar...', + 'sidebar.noResults': 'Sin resultados', + 'sidebar.nodes': 'Nodos', + 'sidebar.components': 'Componentes', + 'sidebar.snippets': 'Fragmentos', + 'sidebar.addons': 'Complementos', + 'mindmap.addChild': 'Agregar subtema', + 'mindmap.addSibling': 'Agregar tema hermano', + 'mindmap.addParent': 'Agregar tema padre', + 'mindmap.delete': 'Eliminar', + 'mindmap.insertAfter': 'Insertar después', + 'mindmap.insertBefore': 'Insertar antes', + 'aiModel.title': 'Modelo de IA', + 'aiModel.provider': 'Proveedor', + 'aiModel.model': 'Modelo', + 'aiModel.temperature': 'Temperatura', + 'aiModel.maxTokens': 'Máximo de tokens', + 'aiModel.buttons.retry': 'Reintentar', + 'aiModel.buttons.stop': 'Detener', + 'aiModel.buttons.clear': 'Limpiar', + 'aiModel.thinking': 'Pensando...', + 'aiModel.error': 'Error de IA', + 'share.title': 'Compartir', + 'share.description': 'Compartir este lienzo', + 'share.copyLink': 'Copiar enlace', + 'share.embed': 'Insertar', + 'share.embedCode': 'Código de inserción', + 'share.export': 'Exportar', + 'share.settings': 'Configuración de compartir', + 'share.anyoneWithLink': 'Cualquiera con el enlace', + 'share.viewOnly': 'Solo lectura', + 'share.canEdit': 'Puede editar', + 'share.disable': 'Desactivar compartir', + 'share.enable': 'Activar compartir', + 'share.invite': 'Invitar', + 'share.pending': 'Pendiente', + 'share.revoke': 'Revocar', + 'share.expired': 'Expirado', + 'settingsModal.title': 'Configuración', + 'settingsModal.search': 'Buscar...', + 'settingsModal.general': 'General', + 'settingsModal.appearance': 'Apariencia', + 'settingsModal.canvas': 'Lienzo', + 'settingsModal.shortcuts': 'Atajos', + 'settingsModal.account': 'Cuenta', + 'settingsModal.about': 'Acerca de', + 'settingsModal.language': 'Idioma', + 'settingsModal.theme': 'Tema', + 'settingsModal.fontSize': 'Tamaño de fuente', + 'settingsModal.fontFamily': 'Familia de fuente', + 'settingsModal.save': 'Guardar', + 'settingsModal.cancel': 'Cancelar', + 'settingsModal.reset': 'Restablecer', + 'settingsModal.confirm': 'Confirmar', + 'settingsModal.ai.model': 'Modelo de IA', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.label': 'Gemini 2.5 Flash Lite', + 'settingsModal.ai.models.gemini.gemini-2.5-flash.label': 'Gemini 2.5 Flash', + 'settingsModal.ai.models.gemini.gemini-2.5-pro.label': 'Gemini 2.5 Pro', + 'settingsModal.ai.models.gemini.gemini-3-flash.label': 'Gemini 3 Flash', + 'settingsModal.ai.models.gemini.gemini-3-pro.label': 'Gemini 3 Pro', + 'settingsModal.ai.models.openai.gpt-5-mini.label': 'GPT-5 Mini', + 'settingsModal.ai.models.openai.gpt-5.label': 'GPT-5', + 'settingsModal.ai.models.openai.gpt-5.2.label': 'GPT-5.2', + 'settingsModal.ai.models.openai.o4-mini.label': 'O4-Mini', + 'settingsModal.ai.models.openai.o3.label': 'O3', + 'settingsModal.ai.models.claude.claude-haiku-4-5.label': 'Claude Haiku 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-5.label': 'Claude Sonnet 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.label': 'Claude Sonnet 4.6', + 'settingsModal.ai.models.claude.claude-opus-4-6.label': 'Claude Opus 4.6', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.label': + 'Llama 4 Maverick', + 'settingsModal.ai.models.groq.qwen/qwen3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.label': 'Llama 3.3 70B', + 'settingsModal.ai.models.nvidia.meta/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.nvidia.nvidia/nemotron-nano-12b-v2-vl.label': 'Nemotron Nano 12B', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.label': 'DeepSeek V3-2', + 'settingsModal.ai.models.nvidia.qwen/qwq-32b.label': 'QwQ 32B', + 'settingsModal.ai.models.nvidia.moonshotai/kimi-k2-thinking.label': 'Kimi K2 Thinking', + 'settingsModal.ai.models.cerebras.gpt-oss-120b.label': 'GPT OSS 120B', + 'settingsModal.ai.models.cerebras.qwen-3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.label': 'Qwen 3 235B', + 'settingsModal.ai.models.cerebras.zai-glm-4.7.label': 'Zai GLM 4.7', + 'settingsModal.ai.models.mistral.mistral-small-latest.label': 'Mistral Small', + 'settingsModal.ai.models.mistral.mistral-medium-latest.label': 'Mistral Medium', + 'settingsModal.ai.models.mistral.mistral-large-latest.label': 'Mistral Large', + 'settingsModal.ai.models.mistral.codestral-latest.label': 'Codestral', + 'settingsModal.ai.models.mistral.pixtral-large-latest.label': 'Pixtral Large', + 'settingsModal.ai.customEndpoints.ollama.name': 'Ollama', + 'settingsModal.ai.customEndpoints.lmStudio.name': 'LM Studio', + 'settingsModal.ai.customEndpoints.together.name': 'Together AI', + 'settingsModal.brand.logo': 'Logo', + 'settingsModal.brand.favicon': 'Favicon', + 'toolbar.flowpilot': 'Flowpilot', + 'contextMenu.label': 'Menú contextual del lienzo', + 'connectMenu.label': 'Menú de conexión de nodos', + 'home.homeEmptyTitle': 'Crea tu primer flujo', + 'home.homeEmptySubtitle': + 'Diseña arquitecturas de nivel empresarial al instante. Comienza con un lienzo en blanco, describe tu infraestructura con nuestro constructor de IA, o usa una plantilla adaptada.', + 'home.homeBlankCanvas': 'Lienzo en blanco', + 'home.homeFlowpilotAI': 'Flowpilot IA', + 'home.homeTemplates': 'Plantillas', + 'home.homeImportFile': 'O importar un archivo existente', + 'commandBar.import.categories.openapi': 'OpenAPI', + 'commandBar.import.infraFormats.terraformState': 'Estado de Terraform', + 'commandBar.import.infraFormats.kubernetes': 'Kubernetes', + 'commandBar.import.infraFormats.dockerCompose': 'Docker Compose', + 'commandBar.layout.normal': 'Normal', + 'commandBar.visuals.bezier': 'Bezier', + 'commandBar.visuals.largeGraphSafetyOn': 'Seguridad de gráfico grande Activada', + 'commandBar.code.quickFixes': '快速修复', + 'commandBar.code.linePrefix': '行 {{line}}: ', + 'commandBar.code.hintPrefix': '提示:', + 'commandBar.code.strictModeGuidance.defineEndpoints': + '在连接边之前,将每个边端点定义为明确的架构节点。', + 'commandBar.code.strictModeGuidance.uniqueIds': + '为每个服务/组/节点使用唯一ID(无重复架构ID)。', + 'commandBar.code.strictModeGuidance.edgeSyntax': + '使用架构边箭头 `-->` , `<--`, 或 `<-->` 和侧面限定符如 `api:R --> L:db`。', + 'commandBar.code.strictModeGuidance.nodeSyntax': + '使用有效的节点声明:`service id(icon)[Label]`,`group id[Label]`,`junction id[Label]`。', + 'commandBar.code.strictModeGuidance.fallback': + '关闭架构严格模式以允许自动恢复,或修复诊断后重试。', + 'settingsModal.canvas.architectureStrictMode': '架构严格模式', + 'settingsModal.canvas.architectureStrictModeDesc': + '当架构诊断包含恢复/验证问题时阻止Mermaid导入', + 'flowCanvas.strictModePasteBlocked': + '架构严格模式阻止了Mermaid粘贴。打开代码视图,修复诊断后重试。', + }, + zh: { + 'nav.beta': '测试版', + 'export.openflowdslLabel': '{{appName}} DSL', + 'landing.nav.figma': 'Figma', + 'home.continueTitle': '继续最近的操作', + 'home.suggestionBlank': '空白画布', + 'home.suggestionBlankDesc': '直接进入编辑器', + 'home.suggestionAI': 'Flowpilot AI', + 'home.suggestionAIDesc': '从提示开始', + 'home.suggestionImport': '导入', + 'home.suggestionImportDesc': '导入现有工作', + 'home.suggestionTemplates': '模板', + 'home.suggestionTemplatesDesc': '从经过验证的模式开始', + 'ai.model': '模型', + 'history.undoUnavailable': '目前没有可撤销的操作。', + 'history.redoUnavailable': '目前没有可重做的操作。', + 'welcome.title': 'OpenFlowKit', + 'welcome.shortcutsTitle': '使用快捷键快速开始', + 'welcome.shortcutsBadge': '键盘优先', + 'welcome.shortcutCommandBar': '打开命令中心', + 'welcome.shortcutHelp': '查看键盘快捷键', + 'welcome.shortcutCanvas': '在画布上添加节点', + 'cta.github': 'GitHub', + 'share.betaBadge': '测试版', + 'chatbot.aiName': 'AI', + 'chatbot.prompts.keyboardShortcuts.icon': '⌘', + 'chatbot.prompts.nodeTypes.icon': '❖', + 'chatbot.prompts.dslSyntax.icon': '{}', + 'commandBar.figmaImport.fileUrlPlaceholder': 'https://www.figma.com/design/...', + 'commandBar.figmaImport.tokenPlaceholder': 'figd_...', + 'commandBar.import.categories.sql': 'SQL', + 'commandBar.import.categories.mindmap': '思维导图', + 'commandBar.import.categories.markdown': 'Markdown', + 'canvas.addNodeShortcut': '添加节点', + 'canvas.aiChatPlaceholder': '创建一些内容...', + 'canvas.nodes': '节点', + 'canvas.connections': '连接', + 'canvas.elements': '元素', + 'canvas.selection': '选择', + 'canvas.noSelection': '无选择', + 'canvas.alignNodes': '对齐节点', + 'canvas.distributeNodes': '分布节点', + 'canvas.alignLeft': '左对齐', + 'canvas.alignCenter': '居中对齐', + 'canvas.alignRight': '右对齐', + 'canvas.alignTop': '顶部对齐', + 'canvas.alignMiddle': '中间对齐', + 'canvas.alignBottom': '底部对齐', + 'canvas.distributeHorizontally': '水平分布', + 'canvas.distributeVertically': '垂直分布', + 'canvas.zoomToFit': '缩放以适应', + 'canvas.zoomToSelection': '缩放到选择', + 'canvas.locked': '已锁定', + 'canvas.unlock': '解锁', + 'canvas.lock': '锁定', + 'canvas.bringToFront': '带到前面', + 'canvas.sendToBack': '发送到后面', + 'canvas.group': '分组', + 'canvas.ungroup': '取消分组', + 'sidebar.searchPlaceholder': '搜索...', + 'sidebar.noResults': '无结果', + 'sidebar.nodes': '节点', + 'sidebar.components': '组件', + 'sidebar.snippets': '代码片段', + 'sidebar.addons': '插件', + 'mindmap.addChild': '添加子主题', + 'mindmap.addSibling': '添加同级主题', + 'mindmap.addParent': '添加父主题', + 'mindmap.delete': '删除', + 'mindmap.insertAfter': '在后面插入', + 'mindmap.insertBefore': '在前面插入', + 'aiModel.title': 'AI模型', + 'aiModel.provider': '提供商', + 'aiModel.model': '模型', + 'aiModel.temperature': '温度', + 'aiModel.maxTokens': '最大令牌数', + 'aiModel.buttons.retry': '重试', + 'aiModel.buttons.stop': '停止', + 'aiModel.buttons.clear': '清除', + 'aiModel.thinking': '思考中...', + 'aiModel.error': 'AI错误', + 'share.title': '分享', + 'share.description': '分享此画布', + 'share.copyLink': '复制链接', + 'share.embed': '嵌入', + 'share.embedCode': '嵌入代码', + 'share.export': '导出', + 'share.settings': '分享设置', + 'share.anyoneWithLink': '拥有链接的任何人', + 'share.viewOnly': '仅查看', + 'share.canEdit': '可以编辑', + 'share.disable': '关闭分享', + 'share.enable': '开启分享', + 'share.invite': '邀请', + 'share.pending': '待处理', + 'share.revoke': '撤销', + 'share.expired': '已过期', + 'settingsModal.title': '设置', + 'settingsModal.search': '搜索...', + 'settingsModal.general': '通用', + 'settingsModal.appearance': '外观', + 'settingsModal.canvas': '画布', + 'settingsModal.shortcuts': '快捷键', + 'settingsModal.account': '账户', + 'settingsModal.about': '关于', + 'settingsModal.language': '语言', + 'settingsModal.theme': '主题', + 'settingsModal.fontSize': '字体大小', + 'settingsModal.fontFamily': '字体', + 'settingsModal.save': '保存', + 'settingsModal.cancel': '取消', + 'settingsModal.reset': '重置', + 'settingsModal.confirm': '确认', + 'settingsModal.ai.model': 'AI模型', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.label': 'Gemini 2.5 Flash Lite', + 'settingsModal.ai.models.gemini.gemini-2.5-flash.label': 'Gemini 2.5 Flash', + 'settingsModal.ai.models.gemini.gemini-2.5-pro.label': 'Gemini 2.5 Pro', + 'settingsModal.ai.models.gemini.gemini-3-flash.label': 'Gemini 3 Flash', + 'settingsModal.ai.models.gemini.gemini-3-pro.label': 'Gemini 3 Pro', + 'settingsModal.ai.models.openai.gpt-5-mini.label': 'GPT-5 Mini', + 'settingsModal.ai.models.openai.gpt-5.label': 'GPT-5', + 'settingsModal.ai.models.openai.gpt-5.2.label': 'GPT-5.2', + 'settingsModal.ai.models.openai.o4-mini.label': 'O4-Mini', + 'settingsModal.ai.models.openai.o3.label': 'O3', + 'settingsModal.ai.models.claude.claude-haiku-4-5.label': 'Claude Haiku 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-5.label': 'Claude Sonnet 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.label': 'Claude Sonnet 4.6', + 'settingsModal.ai.models.claude.claude-opus-4-6.label': 'Claude Opus 4.6', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.label': + 'Llama 4 Maverick', + 'settingsModal.ai.models.groq.qwen/qwen3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.label': 'Llama 3.3 70B', + 'settingsModal.ai.models.nvidia.meta/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.nvidia.nvidia/nemotron-nano-12b-v2-vl.label': 'Nemotron Nano 12B', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.label': 'DeepSeek V3-2', + 'settingsModal.ai.models.nvidia.qwen/qwq-32b.label': 'QwQ 32B', + 'settingsModal.ai.models.nvidia.moonshotai/kimi-k2-thinking.label': 'Kimi K2 Thinking', + 'settingsModal.ai.models.cerebras.gpt-oss-120b.label': 'GPT OSS 120B', + 'settingsModal.ai.models.cerebras.qwen-3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.label': 'Qwen 3 235B', + 'settingsModal.ai.models.cerebras.zai-glm-4.7.label': 'Zai GLM 4.7', + 'settingsModal.ai.models.mistral.mistral-small-latest.label': 'Mistral Small', + 'settingsModal.ai.models.mistral.mistral-medium-latest.label': 'Mistral Medium', + 'settingsModal.ai.models.mistral.mistral-large-latest.label': 'Mistral Large', + 'settingsModal.ai.models.mistral.codestral-latest.label': 'Codestral', + 'settingsModal.ai.models.mistral.pixtral-large-latest.label': 'Pixtral Large', + 'settingsModal.ai.customEndpoints.ollama.name': 'Ollama', + 'settingsModal.ai.customEndpoints.lmStudio.name': 'LM Studio', + 'settingsModal.ai.customEndpoints.together.name': 'Together AI', + 'settingsModal.brand.logo': '标志', + 'settingsModal.brand.favicon': '网站图标', + 'toolbar.flowpilot': 'Flowpilot', + 'contextMenu.label': '画布右键菜单', + 'connectMenu.label': '节点连接菜单', + 'home.homeEmptyTitle': '创建您的第一个流程', + 'home.homeEmptySubtitle': + '即时设计企业级架构。从空白画布开始,用我们的AI构建器描述您的基础设施,或使用定制模板。', + 'home.homeBlankCanvas': '空白画布', + 'home.homeFlowpilotAI': 'Flowpilot 人工智能', + 'home.homeTemplates': '模板', + 'home.homeImportFile': '或导入现有文件', + 'commandBar.import.categories.openapi': 'OpenAPI', + 'commandBar.import.infraFormats.terraformState': 'Terraform状态', + 'commandBar.import.infraFormats.kubernetes': 'Kubernetes', + 'commandBar.import.infraFormats.dockerCompose': 'Docker Compose', + 'commandBar.layout.normal': '普通', + 'commandBar.visuals.bezier': '贝塞尔曲线', + 'commandBar.visuals.largeGraphSafetyOn': '大图安全开启', + 'commandBar.code.quickFixes': 'クイック修正', + 'commandBar.code.linePrefix': '行 {{line}}: ', + 'commandBar.code.hintPrefix': 'ヒント:', + 'commandBar.code.strictModeGuidance.defineEndpoints': + 'エッジを接続する前に、各エンドポイントを明示的なアーキテクチャノードとして定義してください。', + 'commandBar.code.strictModeGuidance.uniqueIds': + '各サービス/グループ/ノードに一意のIDを使用してください(重複するアーキテクチャIDなし)。', + 'commandBar.code.strictModeGuidance.edgeSyntax': + 'アーキテクチャエッジ矢印 `-->` , `<--`, または `<-->` と `api:R --> L:db` などのサイド修飾子を使用してください。', + 'commandBar.code.strictModeGuidance.nodeSyntax': + '有効なノード宣言を使用:`service id(icon)[Label]`, `group id[Label]`, `junction id[Label]`。', + 'commandBar.code.strictModeGuidance.fallback': + '自動回復を許可するにはアーキテクチャstrictモードをオフにするか、診断を修正して再試行してください。', + 'settingsModal.canvas.architectureStrictMode': 'アーキテクチャStrictモード', + 'settingsModal.canvas.architectureStrictModeDesc': + 'アーキテクチャ診断が回復/検証の問題を含む場合、Mermaidインポートをブロック', + 'flowCanvas.strictModePasteBlocked': + 'アーキテクチャstrictモードがMermaid貼り付けをブロックしました。コードビューを開き、診断を修正してから再試行してください。', + }, + ja: { + 'nav.beta': 'ベータ', + 'export.openflowdslLabel': '{{appName}} DSL', + 'landing.nav.figma': 'Figma', + 'home.continueTitle': '最近の操作を続ける', + 'home.suggestionBlank': '空白のキャンバス', + 'home.suggestionBlankDesc': 'エディタに直接ジャンプ', + 'home.suggestionAI': 'Flowpilot AI', + 'home.suggestionAIDesc': 'プロンプトから始める', + 'home.suggestionImport': 'インポート', + 'home.suggestionImportDesc': '既存の作品を持ち込む', + 'home.suggestionTemplates': 'テンプレート', + 'home.suggestionTemplatesDesc': '実証済みのパターンから始める', + 'ai.model': 'モデル', + 'history.undoUnavailable': 'まだ元に戻すものがありません。', + 'history.redoUnavailable': 'やり直すものがありません。', + 'welcome.title': 'OpenFlowKit', + 'welcome.shortcutsTitle': 'ショートカットで素早く開始', + 'welcome.shortcutsBadge': 'キーボードファースト', + 'welcome.shortcutCommandBar': 'コマンドセンターを開く', + 'welcome.shortcutHelp': 'キーボードショートカットを表示', + 'welcome.shortcutCanvas': 'キャンバスにノードを追加', + 'cta.github': 'GitHub', + 'share.betaBadge': 'ベータ', + 'chatbot.aiName': 'AI', + 'chatbot.prompts.keyboardShortcuts.icon': '⌘', + 'chatbot.prompts.nodeTypes.icon': '❖', + 'chatbot.prompts.dslSyntax.icon': '{}', + 'commandBar.figmaImport.fileUrlPlaceholder': 'https://www.figma.com/design/...', + 'commandBar.figmaImport.tokenPlaceholder': 'figd_...', + 'commandBar.import.categories.sql': 'SQL', + 'commandBar.import.categories.mindmap': 'マインドマップ', + 'commandBar.import.categories.markdown': 'Markdown', + 'canvas.addNodeShortcut': 'ノードを追加', + 'canvas.aiChatPlaceholder': '何かを作成...', + 'canvas.nodes': 'ノード', + 'canvas.connections': '接続', + 'canvas.elements': '要素', + 'canvas.selection': '選択', + 'canvas.noSelection': '選択なし', + 'canvas.alignNodes': 'ノードを整列', + 'canvas.distributeNodes': 'ノードを分布', + 'canvas.alignLeft': '左揃え', + 'canvas.alignCenter': '中央揃え', + 'canvas.alignRight': '右揃え', + 'canvas.alignTop': '上揃え', + 'canvas.alignMiddle': '中間揃え', + 'canvas.alignBottom': '下揃え', + 'canvas.distributeHorizontally': '水平に分布', + 'canvas.distributeVertically': '垂直に分布', + 'canvas.zoomToFit': 'フィットにズーム', + 'canvas.zoomToSelection': '選択にズーム', + 'canvas.locked': 'ロック済み', + 'canvas.unlock': 'ロック解除', + 'canvas.lock': 'ロック', + 'canvas.bringToFront': '最前面へ', + 'canvas.sendToBack': '最背面へ', + 'canvas.group': 'グループ化', + 'canvas.ungroup': 'グループ解除', + 'sidebar.searchPlaceholder': '検索...', + 'sidebar.noResults': '結果なし', + 'sidebar.nodes': 'ノード', + 'sidebar.components': 'コンポーネント', + 'sidebar.snippets': 'スニペット', + 'sidebar.addons': 'アドオン', + 'mindmap.addChild': 'サブトピックを追加', + 'mindmap.addSibling': '兄弟トピックを追加', + 'mindmap.addParent': '親トピックを追加', + 'mindmap.delete': '削除', + 'mindmap.insertAfter': '後に挿入', + 'mindmap.insertBefore': '前に挿入', + 'aiModel.title': 'AIモデル', + 'aiModel.provider': 'プロバイダー', + 'aiModel.model': 'モデル', + 'aiModel.temperature': '温度', + 'aiModel.maxTokens': '最大トークン数', + 'aiModel.buttons.retry': '再試行', + 'aiModel.buttons.stop': '停止', + 'aiModel.buttons.clear': 'クリア', + 'aiModel.thinking': '思考中...', + 'aiModel.error': 'AIエラー', + 'share.title': '共有', + 'share.description': 'このキャンバスを共有', + 'share.copyLink': 'リンクをコピー', + 'share.embed': '埋め込み', + 'share.embedCode': '埋め込みコード', + 'share.export': 'エクスポート', + 'share.settings': '共有設定', + 'share.anyoneWithLink': 'リンクを持つ任何人', + 'share.viewOnly': '表示のみ', + 'share.canEdit': '編集可能', + 'share.disable': '共有を無効化', + 'share.enable': '共有を有効化', + 'share.invite': '招待', + 'share.pending': '保留中', + 'share.revoke': '取り消し', + 'share.expired': '期限切れ', + 'settingsModal.title': '設定', + 'settingsModal.search': '検索...', + 'settingsModal.general': '一般', + 'settingsModal.appearance': '外観', + 'settingsModal.canvas': 'キャンバス', + 'settingsModal.shortcuts': 'ショートカット', + 'settingsModal.account': 'アカウント', + 'settingsModal.about': '概要', + 'settingsModal.language': '言語', + 'settingsModal.theme': 'テーマ', + 'settingsModal.fontSize': 'フォントサイズ', + 'settingsModal.fontFamily': 'フォント', + 'settingsModal.save': '保存', + 'settingsModal.cancel': 'キャンセル', + 'settingsModal.reset': 'リセット', + 'settingsModal.confirm': '確認', + 'settingsModal.ai.model': 'AIモデル', + 'settingsModal.ai.models.gemini.gemini-2.5-flash-lite.label': 'Gemini 2.5 Flash Lite', + 'settingsModal.ai.models.gemini.gemini-2.5-flash.label': 'Gemini 2.5 Flash', + 'settingsModal.ai.models.gemini.gemini-2.5-pro.label': 'Gemini 2.5 Pro', + 'settingsModal.ai.models.gemini.gemini-3-flash.label': 'Gemini 3 Flash', + 'settingsModal.ai.models.gemini.gemini-3-pro.label': 'Gemini 3 Pro', + 'settingsModal.ai.models.openai.gpt-5-mini.label': 'GPT-5 Mini', + 'settingsModal.ai.models.openai.gpt-5.label': 'GPT-5', + 'settingsModal.ai.models.openai.gpt-5.2.label': 'GPT-5.2', + 'settingsModal.ai.models.openai.o4-mini.label': 'O4-Mini', + 'settingsModal.ai.models.openai.o3.label': 'O3', + 'settingsModal.ai.models.claude.claude-haiku-4-5.label': 'Claude Haiku 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-5.label': 'Claude Sonnet 4.5', + 'settingsModal.ai.models.claude.claude-sonnet-4-6.label': 'Claude Sonnet 4.6', + 'settingsModal.ai.models.claude.claude-opus-4-6.label': 'Claude Opus 4.6', + 'settingsModal.ai.models.groq.meta-llama/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.groq.meta-llama/llama-4-maverick-17b-128e-instruct.label': + 'Llama 4 Maverick', + 'settingsModal.ai.models.groq.qwen/qwen3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.groq.llama-3.3-70b-versatile.label': 'Llama 3.3 70B', + 'settingsModal.ai.models.nvidia.meta/llama-4-scout-17b-16e-instruct.label': 'Llama 4 Scout', + 'settingsModal.ai.models.nvidia.nvidia/nemotron-nano-12b-v2-vl.label': 'Nemotron Nano 12B', + 'settingsModal.ai.models.nvidia.deepseek/deepseek-v3-2.label': 'DeepSeek V3-2', + 'settingsModal.ai.models.nvidia.qwen/qwq-32b.label': 'QwQ 32B', + 'settingsModal.ai.models.nvidia.moonshotai/kimi-k2-thinking.label': 'Kimi K2 Thinking', + 'settingsModal.ai.models.cerebras.gpt-oss-120b.label': 'GPT OSS 120B', + 'settingsModal.ai.models.cerebras.qwen-3-32b.label': 'Qwen 3 32B', + 'settingsModal.ai.models.cerebras.qwen-3-235b-a22b.label': 'Qwen 3 235B', + 'settingsModal.ai.models.cerebras.zai-glm-4.7.label': 'Zai GLM 4.7', + 'settingsModal.ai.models.mistral.mistral-small-latest.label': 'Mistral Small', + 'settingsModal.ai.models.mistral.mistral-medium-latest.label': 'Mistral Medium', + 'settingsModal.ai.models.mistral.mistral-large-latest.label': 'Mistral Large', + 'settingsModal.ai.models.mistral.codestral-latest.label': 'Codestral', + 'settingsModal.ai.models.mistral.pixtral-large-latest.label': 'Pixtral Large', + 'settingsModal.ai.customEndpoints.ollama.name': 'Ollama', + 'settingsModal.ai.customEndpoints.lmStudio.name': 'LM Studio', + 'settingsModal.ai.customEndpoints.together.name': 'Together AI', + 'settingsModal.brand.logo': 'ロゴ', + 'settingsModal.brand.favicon': 'ファビコン', + 'toolbar.flowpilot': 'Flowpilot', + 'contextMenu.label': 'キャンバスコンテキストメニュー', + 'connectMenu.label': 'ノード接続メニュー', + 'home.homeEmptyTitle': '最初のフローを作成', + 'home.homeEmptySubtitle': + 'エンタープライズグレードのアーキテクチャを即座に設計。空白のキャンバスから始めるか、AIビルダーでインフラを描述するか、カスタマイズテンプレートを使用してください。', + 'home.homeBlankCanvas': '空白のキャンバス', + 'home.homeFlowpilotAI': 'Flowpilot AI', + 'home.homeTemplates': 'テンプレート', + 'home.homeImportFile': 'または既存のファイルをインポート', + 'commandBar.import.categories.openapi': 'OpenAPI', + 'commandBar.import.infraFormats.terraformState': 'Terraform状態', + 'commandBar.import.infraFormats.kubernetes': 'Kubernetes', + 'commandBar.import.infraFormats.dockerCompose': 'Docker Compose', + 'commandBar.layout.normal': '標準', + 'commandBar.visuals.bezier': 'ベジェ曲線', + 'commandBar.visuals.largeGraphSafetyOn': '大規模グラフ安全モードオン', + }, +}; + +function setNestedValue(obj, path, value) { + const keys = path.split('.'); + let current = obj; + for (let i = 0; i < keys.length - 1; i++) { + if (!current[keys[i]]) current[keys[i]] = {}; + current = current[keys[i]]; + } + current[keys[keys.length - 1]] = value; +} + +Object.entries(translations).forEach(([locale, trans]) => { + Object.entries(trans).forEach(([key, value]) => { + setNestedValue(locales[locale], key, value); + }); +}); + +Object.entries(locales).forEach(([locale, data]) => { + fs.writeFileSync(`src/i18n/locales/${locale}/translation.json`, JSON.stringify(data, null, 2)); + console.log(`Updated ${locale} translations`); +}); + +console.log('Done!'); diff --git a/src/App.tsx b/src/App.tsx index 140a0e8..c38cf02 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,6 @@ import { DocsSiteRedirect } from '@/components/app/DocsSiteRedirect'; import { RouteLoadingFallback } from '@/components/app/RouteLoadingFallback'; import { MobileWorkspaceGate } from '@/components/app/MobileWorkspaceGate'; import { CinematicExportProvider } from '@/context/CinematicExportContext'; -import { HomePage } from '@/components/HomePage'; import { useFlowStore } from './store'; import { useEditorPageActions } from '@/store/editorPageHooks'; @@ -36,6 +35,11 @@ async function loadFlowEditorModule() { const FlowEditor = lazy(loadFlowEditorModule); +const LazyHomePage = lazy(async () => { + const module = await import('./components/HomePage'); + return { default: module.HomePage }; +}); + const LazyKeyboardShortcutsModal = lazy(async () => { const module = await import('./components/KeyboardShortcutsModal'); return { default: module.KeyboardShortcutsModal }; @@ -121,16 +125,18 @@ function HomePageRoute(): React.JSX.Element { } return ( - navigate(`/flow/${flowId}`)} - activeTab={activeTab} - onSwitchTab={(tab) => navigate(getHomePagePath(tab))} - /> + }> + navigate(`/flow/${flowId}`)} + activeTab={activeTab} + onSwitchTab={(tab) => navigate(getHomePagePath(tab))} + /> + ); } diff --git a/src/app/routeState.test.ts b/src/app/routeState.test.ts index ce049b6..33bcf6e 100644 --- a/src/app/routeState.test.ts +++ b/src/app/routeState.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import { createFlowEditorAIRouteState, createFlowEditorImportRouteState, - createFlowEditorTemplatesRouteState, shouldOpenFlowEditorAI, shouldOpenFlowEditorImportDialog, shouldOpenFlowEditorTemplates, @@ -14,8 +13,8 @@ describe('routeState', () => { }); it('creates route state that requests templates and studio ai entry points', () => { - expect(createFlowEditorTemplatesRouteState()).toEqual({ openTemplates: true }); expect(createFlowEditorAIRouteState()).toEqual({ openStudioAI: true }); + expect(shouldOpenFlowEditorTemplates({ openTemplates: true })).toBe(true); }); it('detects only valid import-dialog route state payloads', () => { diff --git a/src/app/routeState.ts b/src/app/routeState.ts index 285bdc8..dbd42ab 100644 --- a/src/app/routeState.ts +++ b/src/app/routeState.ts @@ -9,10 +9,6 @@ export function createFlowEditorImportRouteState(): FlowEditorRouteState { return { openImportDialog: true }; } -export function createFlowEditorTemplatesRouteState(): FlowEditorRouteState { - return { openTemplates: true }; -} - export function createFlowEditorInitialTemplateRouteState(templateId: string): FlowEditorRouteState { return { initialTemplateId: templateId }; } diff --git a/src/components/CommandBar.test.tsx b/src/components/CommandBar.test.tsx index edcf729..daa1155 100644 --- a/src/components/CommandBar.test.tsx +++ b/src/components/CommandBar.test.tsx @@ -4,7 +4,22 @@ import { describe, expect, it, vi } from 'vitest'; import { CommandBar } from './CommandBar'; vi.mock('./command-bar/useCommandBarCommands', () => ({ - useCommandBarCommands: () => [], + useCommandBarCommands: () => [ + { + id: 'command-1', + label: 'Open AI', + description: 'Open AI tools', + type: 'action', + action: vi.fn(), + }, + { + id: 'command-2', + label: 'Open Search', + description: 'Open search tools', + type: 'navigation', + view: 'search', + }, + ], })); describe('CommandBar', () => { @@ -19,7 +34,7 @@ describe('CommandBar', () => { render(); expect(screen.getByRole('dialog', { name: 'Command bar' })).toBeTruthy(); - expect(document.activeElement).toBe(screen.getByRole('textbox', { name: 'Search command bar actions' })); + expect(document.activeElement).toBe(screen.getByRole('combobox', { name: 'Search command bar actions' })); }); it('closes on Escape and restores focus to the previous control', async () => { @@ -48,4 +63,15 @@ describe('CommandBar', () => { expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Open command bar' })); }); }); + + it('wires the search input to the active command option for assistive tech', () => { + render(); + + const input = screen.getByRole('combobox', { name: 'Search command bar actions' }); + fireEvent.keyDown(window, { key: 'ArrowDown' }); + + expect(input.getAttribute('aria-controls')).toBeTruthy(); + expect(input.getAttribute('aria-activedescendant')).toContain('-option-0'); + expect(screen.getByRole('listbox')).toBeTruthy(); + }); }); diff --git a/src/components/ConnectMenu.test.tsx b/src/components/ConnectMenu.test.tsx index 4f3dcc9..834ef9d 100644 --- a/src/components/ConnectMenu.test.tsx +++ b/src/components/ConnectMenu.test.tsx @@ -93,7 +93,7 @@ describe('ConnectMenu', () => { /> ); - expect(await screen.findByRole('button', { name: /Analytics Glue/i })).toBeTruthy(); + expect(await screen.findByRole('menuitem', { name: /Analytics Glue/i })).toBeTruthy(); expect(screen.queryByText('connectMenu.process')).toBeNull(); }); @@ -135,4 +135,25 @@ describe('ConnectMenu', () => { data: { condition: 'yes' }, }); }); + + it('exposes a keyboard-navigable menu and closes on escape', () => { + const onClose = vi.fn(); + + render( + + ); + + const menu = screen.getByRole('menu', { name: 'Connect node menu' }); + fireEvent.keyDown(menu, { key: 'ArrowDown' }); + fireEvent.keyDown(menu, { key: 'Escape' }); + + expect(onClose).toHaveBeenCalled(); + }); }); diff --git a/src/components/ConnectMenu.tsx b/src/components/ConnectMenu.tsx index d35e4d5..4629a31 100644 --- a/src/components/ConnectMenu.tsx +++ b/src/components/ConnectMenu.tsx @@ -7,9 +7,14 @@ import type { DomainLibraryCategory, DomainLibraryItem } from '@/services/domain import { loadDomainAssetSuggestions } from '@/services/assetCatalog'; import { getAssetCategoryDisplayName } from '@/services/assetPresentation'; import { loadProviderShapePreview } from '@/services/shapeLibrary/providerCatalog'; -import { Tooltip } from './Tooltip'; -import { NamedIcon } from './IconMap'; import type { ConnectedEdgePreset } from '@/hooks/edge-operations/utils'; +import { useMenuKeyboardNavigation } from '@/hooks/useMenuKeyboardNavigation'; +import { + type ConnectMenuOption, + GenericConnectOptionsSection, + MindmapConnectSection, + ProviderSuggestionsSection, +} from './ConnectMenuSections'; interface ConnectMenuProps { position: { x: number; y: number }; @@ -20,16 +25,6 @@ interface ConnectMenuProps { onClose: () => void; } -interface ConnectMenuOption { - type: string; - shape?: string; - edgePreset?: ConnectedEdgePreset; - title: string; - description: string; - toneClassName: string; - icon: React.ReactNode; -} - function getContextualOptions(sourceType?: string | null): ConnectMenuOption[] { switch (sourceType) { case 'class': @@ -98,6 +93,8 @@ function getContextualOptions(sourceType?: string | null): ConnectMenuOption[] { export const ConnectMenu = ({ position, sourceId, sourceType, onSelect, onSelectAsset, onClose }: ConnectMenuProps): React.ReactElement => { const { t } = useTranslation(); + const menuRef = React.useRef(null); + const { onKeyDown } = useMenuKeyboardNavigation({ menuRef, onClose }); const sourceNode = useFlowStore((state) => state.nodes.find((node) => node.id === sourceId)); const isMindmapSource = isMindmapConnectorSource(sourceType); const isAssetSource = sourceNode?.data?.assetPresentation === 'icon' @@ -255,8 +252,13 @@ export const ConnectMenu = ({ position, sourceId, sourceType, onSelect, onSelect className="fixed inset-0 z-[60]" onClick={onClose} aria-label="Close connect menu" + tabIndex={-1} />
@@ -266,65 +268,23 @@ export const ConnectMenu = ({ position, sourceId, sourceType, onSelect, onSelect
{isMindmapSource ? ( - + handleSelect('mindmap')} + /> ) : isAssetSource && providerItems.length > 0 ? ( - <> -
-
- {providerTitle} suggestions -
-
-
-
- {providerItems.map((item) => ( - - - - ))} -
-
- + ) : ( - <> - {menuOptions.map((option) => ( - - ))} - + )} diff --git a/src/components/ConnectMenuSections.tsx b/src/components/ConnectMenuSections.tsx new file mode 100644 index 0000000..f8e0787 --- /dev/null +++ b/src/components/ConnectMenuSections.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { Database, Settings } from 'lucide-react'; +import { Tooltip } from './Tooltip'; +import { NamedIcon } from './IconMap'; +import type { DomainLibraryItem } from '@/services/domainLibrary'; +import type { ConnectedEdgePreset } from '@/hooks/edge-operations/utils'; + +export interface ConnectMenuOption { + type: string; + shape?: string; + edgePreset?: ConnectedEdgePreset; + title: string; + description: string; + toneClassName: string; + icon: React.ReactNode; +} + +interface MindmapConnectSectionProps { + title: string; + description: string; + onSelect: () => void; +} + +export function MindmapConnectSection({ + title, + description, + onSelect, +}: MindmapConnectSectionProps): React.ReactElement { + return ( + + ); +} + +interface ProviderSuggestionsSectionProps { + title: string; + items: DomainLibraryItem[]; + previewUrls: Record; + onSelectAsset: (item: DomainLibraryItem) => void; +} + +export function ProviderSuggestionsSection({ + title, + items, + previewUrls, + onSelectAsset, +}: ProviderSuggestionsSectionProps): React.ReactElement { + return ( + <> +
+
+ {title} +
+
+
+
+ {items.map((item) => ( + + + + ))} +
+
+ + ); +} + +interface GenericConnectOptionsSectionProps { + options: ConnectMenuOption[]; + onSelect: (type: string, shape?: string, edgePreset?: ConnectedEdgePreset) => void; +} + +export function GenericConnectOptionsSection({ + options, + onSelect, +}: GenericConnectOptionsSectionProps): React.ReactElement { + return ( + <> + {options.map((option) => ( + + ))} + + ); +} diff --git a/src/components/ContextMenu.test.tsx b/src/components/ContextMenu.test.tsx index 34362e9..7a6d23e 100644 --- a/src/components/ContextMenu.test.tsx +++ b/src/components/ContextMenu.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { ContextMenu, getContextMenuPosition } from './ContextMenu'; @@ -36,4 +36,28 @@ describe('ContextMenu', () => { }) ).toEqual({ x: 88, y: 48 }); }); + + it('supports keyboard navigation and escape close', () => { + const onClose = vi.fn(); + + render( + + ); + + const menu = screen.getByRole('menu', { name: 'Canvas context menu' }); + fireEvent.keyDown(menu, { key: 'ArrowDown' }); + fireEvent.keyDown(menu, { key: 'Escape' }); + + expect(onClose).toHaveBeenCalled(); + }); }); diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index d690d56..d118710 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -26,6 +26,7 @@ import { ArrowUpRight, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import { useMenuKeyboardNavigation } from '@/hooks/useMenuKeyboardNavigation'; const VIEWPORT_PADDING = 12; const MENU_BUTTON_CLASS_NAME = @@ -124,6 +125,7 @@ export function ContextMenu({ const { t } = useTranslation(); const menuRef = useRef(null); const [menuPosition, setMenuPosition] = useState(position); + const { onKeyDown } = useMenuKeyboardNavigation({ menuRef, onClose }); useEffect(() => { function handleClickOutside(event: MouseEvent): void { @@ -163,17 +165,22 @@ export function ContextMenu({
{type === 'node' && ( <> )}
diff --git a/src/components/CustomNodeContent.tsx b/src/components/CustomNodeContent.tsx new file mode 100644 index 0000000..da64854 --- /dev/null +++ b/src/components/CustomNodeContent.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import MemoizedMarkdown from './MemoizedMarkdown'; +import { InlineTextEditSurface } from './InlineTextEditSurface'; +import { NamedIcon } from './IconMap'; + +interface InlineEditState { + isEditing: boolean; + draft: string; + beginEdit: () => void; + setDraft: (value: string) => void; + commit: () => void; + handleKeyDown: React.KeyboardEventHandler; +} + +interface CustomNodeContentProps { + data: { + label?: string; + subLabel?: string; + imageUrl?: string; + }; + hasIcon: boolean; + hasSubLabel: boolean; + resolvedAssetIconUrl: string | null | undefined; + iconName: string | null; + iconSizeClassName: string; + iconImageSizeClassName: string; + namedIconSizeClassName: string; + iconBackgroundColor: string; + iconColor: string; + textAlignStyle: React.CSSProperties; + textClassName: string; + textStyle: React.CSSProperties; + subTextClassName: string; + subTextStyle: React.CSSProperties; + displayLabel: React.ReactNode; + labelEdit: InlineEditState; + subLabelEdit: InlineEditState; + hasLabelSelection: boolean; + hasSubLabelSelection: boolean; + lodPreserveClassName: string; + isCompactNode: boolean; + isComplexShape: boolean; + complexShapePaddingClassName: string; + contentPadding: string; +} + +export function CustomNodeContent({ + data, + hasIcon, + hasSubLabel, + resolvedAssetIconUrl, + iconName, + iconSizeClassName, + iconImageSizeClassName, + namedIconSizeClassName, + iconBackgroundColor, + iconColor, + textAlignStyle, + textClassName, + textStyle, + subTextClassName, + subTextStyle, + displayLabel, + labelEdit, + subLabelEdit, + hasLabelSelection, + hasSubLabelSelection, + lodPreserveClassName, + isCompactNode, + isComplexShape, + complexShapePaddingClassName, + contentPadding, +}: CustomNodeContentProps): React.ReactElement { + return ( +
+ {hasIcon ? ( +
+ {resolvedAssetIconUrl ? ( +
+ icon +
+ ) : null} + {iconName ? ( +
+ +
+ ) : null} +
+ ) : null} + +
+ + {hasSubLabel ? ( + } + onBeginEdit={subLabelEdit.beginEdit} + onDraftChange={subLabelEdit.setDraft} + onCommit={subLabelEdit.commit} + onKeyDown={subLabelEdit.handleKeyDown} + className={subTextClassName} + style={subTextStyle} + inputClassName="text-center" + isSelected={hasSubLabelSelection} + /> + ) : null} +
+ + {data.imageUrl ? ( +
+ attachment +
+ ) : null} +
+ ); +} diff --git a/src/components/ExportMenuPanel.tsx b/src/components/ExportMenuPanel.tsx index 14cc9d5..8d634b4 100644 --- a/src/components/ExportMenuPanel.tsx +++ b/src/components/ExportMenuPanel.tsx @@ -10,6 +10,7 @@ import { Film, GitBranch, Image, + Share2, Wand2, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -157,7 +158,7 @@ export function ExportMenuPanel({ }, { key: 'video', - title: t('export.sectionVideo', 'Video & Animation'), + title: t('export.sectionVideo', 'Video'), items: [ { key: 'cinematic-video', @@ -177,7 +178,7 @@ export function ExportMenuPanel({ }, { key: 'code', - title: t('export.sectionCode', 'Code & Data'), + title: t('export.sectionCode', 'Code'), items: [ { key: 'json', @@ -217,6 +218,13 @@ export function ExportMenuPanel({ Icon: Figma, actions: ['download', 'copy'], }, + { + key: 'share', + label: t('export.shareEmbed', 'Share & Embed'), + hint: t('export.hintShareEmbed', 'Read-only viewer link'), + Icon: Share2, + actions: ['download'], + }, ], }, ], diff --git a/src/components/FlowCanvas.tsx b/src/components/FlowCanvas.tsx index 5037253..6d38893 100644 --- a/src/components/FlowCanvas.tsx +++ b/src/components/FlowCanvas.tsx @@ -1,20 +1,13 @@ import React, { useCallback, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useShallow } from 'zustand/react/shallow'; -import ReactFlow, { - Background, - useReactFlow, - toFlowNode, -} from '@/lib/reactflowCompat'; +import { useReactFlow, toFlowNode } from '@/lib/reactflowCompat'; import { useFlowStore } from '../store'; import type { FlowNode, NodeData } from '../lib/types'; import { useFlowOperations } from '../hooks/useFlowOperations'; import { useModifierKeys } from '../hooks/useModifierKeys'; import { useEdgeInteractions } from '../hooks/useEdgeInteractions'; -import CustomConnectionLine from './CustomConnectionLine'; -import { NavigationControls } from './NavigationControls'; -import { FlowCanvasOverlays } from './flow-canvas/FlowCanvasOverlays'; -import { flowCanvasEdgeTypes, flowCanvasNodeTypes } from './flow-canvas/flowCanvasTypes'; +import { FlowCanvasSurface } from './flow-canvas/FlowCanvasSurface'; import { useFlowCanvasMenusAndActions } from './flow-canvas/useFlowCanvasMenusAndActions'; import { useFlowCanvasDragDrop } from './flow-canvas/useFlowCanvasDragDrop'; import { useFlowCanvasConnectionState } from './flow-canvas/useFlowCanvasConnectionState'; @@ -81,6 +74,7 @@ export const FlowCanvas: React.FC = ({ largeGraphSafetyProfile, }); const reactFlowWrapper = useRef(null); + const lastInteractionScreenPositionRef = useRef<{ x: number; y: number } | null>(null); const connectMenuSetterRef = useRef<((value: ConnectMenuState | null) => void) | null>(null); const { screenToFlowPosition, fitView } = useReactFlow(); @@ -204,6 +198,14 @@ export const FlowCanvas: React.FC = ({ y: rect.top + rect.height / 2, }); }; + const getLastInteractionFlowPosition = (): { x: number; y: number } | null => { + const position = lastInteractionScreenPositionRef.current; + if (!position) { + return null; + } + + return screenToFlowPosition(position); + }; const { alignmentGuides, @@ -253,6 +255,7 @@ export const FlowCanvas: React.FC = ({ 'Architecture strict mode blocked Mermaid paste. Open Code view, fix diagnostics, then retry.' ), pasteSelection, + getLastInteractionFlowPosition, getCanvasCenterFlowPosition, }); @@ -263,89 +266,66 @@ export const FlowCanvas: React.FC = ({ }; }, [interactionLowDetailModeActive]); + const selectedNodeCount = nodes.filter((node) => node.selected).length; + const selectedEdgeCount = edges.filter((edge) => edge.selected).length; + const selectionAnnouncement = + selectedNodeCount === 0 && selectedEdgeCount === 0 + ? 'Canvas selection cleared.' + : `${selectedNodeCount} node${selectedNodeCount === 1 ? '' : 's'} and ${selectedEdgeCount} edge${selectedEdgeCount === 1 ? '' : 's'} selected.`; + return ( -
{ + lastInteractionScreenPositionRef.current = { + x: event.clientX, + y: event.clientY, + }; + }} onPasteCapture={handleCanvasPaste} onDoubleClickCapture={onCanvasDoubleClickCapture} - > - - {effectiveShowGrid && ( - - )} - - - -
+ selectionAnnouncement={selectionAnnouncement} + nodes={layerAdjustedNodes} + edges={effectiveEdges} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + onConnect={onConnect} + onReconnect={onReconnect} + onSelectionChange={onSelectionChange} + onNodeDragStart={handleNodeDragStart} + onNodeDrag={handleNodeDrag} + onNodeDragStop={handleNodeDragStop} + onMoveStart={startInteractionLowDetail} + onMoveEnd={endInteractionLowDetail} + onNodeDoubleClick={onNodeDoubleClick} + onNodeClick={onCanvasEntityIntent} + onEdgeClick={onCanvasEntityIntent} + onNodeContextMenu={onNodeContextMenu} + onSelectionContextMenu={onSelectionContextMenu} + onPaneContextMenu={onPaneContextMenu} + onEdgeContextMenu={onEdgeContextMenu} + onPaneClick={onPaneClick} + onConnectStart={onConnectStartWrapper} + onConnectEnd={onConnectEndWrapper} + onDragOver={onDragOver} + onDrop={onDrop} + fitView={true} + reactFlowConfig={reactFlowConfig} + snapToGrid={snapToGrid} + effectiveShowGrid={effectiveShowGrid} + alignmentGuidesEnabled={alignmentGuidesEnabled} + alignmentGuides={alignmentGuides} + selectionDragPreview={selectionDragPreview} + connectMenu={connectMenu} + setConnectMenu={setConnectMenu} + screenToFlowPosition={screenToFlowPosition} + handleAddAndConnect={handleAddAndConnect} + handleAddDomainLibraryItemAndConnect={handleAddDomainLibraryItemAndConnect} + contextMenu={contextMenu} + onCloseContextMenu={onCloseContextMenu} + copySelection={copySelection} + contextActions={contextActions} + /> ); }; diff --git a/src/components/FlowEditor.tsx b/src/components/FlowEditor.tsx index 60e75d6..e26c62d 100644 --- a/src/components/FlowEditor.tsx +++ b/src/components/FlowEditor.tsx @@ -7,6 +7,7 @@ import { ArchitectureLintProvider } from '@/context/ArchitectureLintContext'; import { useCinematicExportState } from '@/context/CinematicExportContext'; import { DiagramDiffProvider } from '@/context/DiagramDiffContext'; import { ShareEmbedModal } from '@/components/ShareEmbedModal'; +import { ImportRecoveryDialog } from '@/components/ImportRecoveryDialog'; const CINEMATIC_EXPORT_BACKGROUND = 'radial-gradient(circle at top, rgba(59,130,246,0.14), transparent 42%), linear-gradient(180deg, #f8fbff 0%, #eef5ff 52%, #f8fafc 100%)'; @@ -29,7 +30,10 @@ export function FlowEditor({ onGoHome }: FlowEditorProps) { isSelectMode, reactFlowWrapper, fileInputRef, + handleImportJSON, onFileImport, + importRecoveryState, + dismissImportRecovery, shareViewerUrl, clearShareViewerUrl, collaborationEnabled, @@ -92,6 +96,14 @@ export function FlowEditor({ onGoHome }: FlowEditorProps) { {shareViewerUrl && ( )} + {importRecoveryState ? ( + + ) : null} diff --git a/src/components/FlowEditorEmptyState.tsx b/src/components/FlowEditorEmptyState.tsx index f1cfc31..a67cb08 100644 --- a/src/components/FlowEditorEmptyState.tsx +++ b/src/components/FlowEditorEmptyState.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import { Keyboard } from 'lucide-react'; import { Button } from './ui/Button'; +import { useShortcutHelpActions } from '@/store/viewHooks'; interface FlowEditorEmptyStateProps { title: string; @@ -23,6 +25,8 @@ export function FlowEditorEmptyState({ onTemplates, onAddNode, }: FlowEditorEmptyStateProps): React.ReactElement { + const { setShortcutsHelpOpen } = useShortcutHelpActions(); + return (
@@ -74,9 +78,18 @@ export function FlowEditorEmptyState({
-
- Press ⌘K for command center -
+
diff --git a/src/components/FlowEditorPanels.test.tsx b/src/components/FlowEditorPanels.test.tsx index ec0aaa6..4e2f159 100644 --- a/src/components/FlowEditorPanels.test.tsx +++ b/src/components/FlowEditorPanels.test.tsx @@ -2,6 +2,11 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { FlowEditorPanels } from './FlowEditorPanels'; +const commandBarShouldThrow = false; +let snapshotsShouldThrow = false; +let propertiesShouldThrow = false; +const studioShouldThrow = false; + vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, fallback?: string) => fallback ?? key, @@ -10,19 +15,43 @@ vi.mock('react-i18next', () => ({ })); vi.mock('./CommandBar', () => ({ - CommandBar: () =>
, + CommandBar: () => { + if (commandBarShouldThrow) { + throw new Error('command-bar exploded'); + } + + return
; + }, })); vi.mock('./SnapshotsPanel', () => ({ - SnapshotsPanel: () =>
, + SnapshotsPanel: () => { + if (snapshotsShouldThrow) { + throw new Error('snapshots exploded'); + } + + return
; + }, })); vi.mock('./PropertiesPanel', () => ({ - PropertiesPanel: () =>
, + PropertiesPanel: () => { + if (propertiesShouldThrow) { + throw new Error('properties exploded'); + } + + return
; + }, })); vi.mock('./StudioPanel', () => ({ - StudioPanel: () =>
, + StudioPanel: () => { + if (studioShouldThrow) { + throw new Error('studio exploded'); + } + + return
; + }, })); const baseProps = { @@ -60,6 +89,9 @@ const baseProps = { onSaveSnapshot: vi.fn(), onRestoreSnapshot: vi.fn(), onDeleteSnapshot: vi.fn(), + historyPastCount: 0, + historyFutureCount: 0, + onScrubHistoryTo: vi.fn(), }, properties: { selectedNode: null, @@ -139,6 +171,41 @@ const selectedNode = { } as const; describe('FlowEditorPanels', () => { + it('keeps the snapshots panel isolated when the properties rail crashes', async () => { + propertiesShouldThrow = true; + + render( + + ); + + expect(await screen.findByText('Properties unavailable')).not.toBeNull(); + expect(await screen.findByTestId('snapshots-panel')).not.toBeNull(); + + propertiesShouldThrow = false; + }); + + it('shows a recoverable fallback when snapshots rendering fails', async () => { + snapshotsShouldThrow = true; + + render( + + ); + + expect(await screen.findByText('Snapshots unavailable')).not.toBeNull(); + expect(screen.getByRole('button', { name: 'Close panel' })).not.toBeNull(); + + snapshotsShouldThrow = false; + }); + it('shows the properties panel in canvas mode', async () => { render( { expect(await screen.findByTestId('properties-panel')).not.toBeNull(); }); + + it('shows the snapshots panel when history is open', async () => { + render( + + ); + + expect(await screen.findByTestId('snapshots-panel')).not.toBeNull(); + }); }); diff --git a/src/components/FlowEditorPanels.tsx b/src/components/FlowEditorPanels.tsx index 16db181..691b50f 100644 --- a/src/components/FlowEditorPanels.tsx +++ b/src/components/FlowEditorPanels.tsx @@ -20,6 +20,12 @@ import type { AIReadinessState } from '@/hooks/ai-generation/readiness'; import type { PropertiesPanel as PropertiesPanelComponent } from './PropertiesPanel'; +interface PanelErrorFallbackProps { + title: string; + description: string; + onClose?: () => void; +} + function CommandBarSkeleton(): React.ReactElement { return (
@@ -34,6 +40,60 @@ function CommandBarSkeleton(): React.ReactElement { ); } +function RailPanelSkeleton(props: { + title: string; + lines?: number; +}): React.ReactElement { + const { title, lines = 5 } = props; + + return ( +
+
+
+
+
+
+
+ {title} +
+
+ {Array.from({ length: lines }, (_, index) => ( +
+ ))} +
+
+
+ ); +} + +function PanelErrorFallback({ + title, + description, + onClose, +}: PanelErrorFallbackProps): React.ReactElement { + return ( +
+
+

+ {title} +

+

+ {description} +

+
+ {onClose ? ( + + ) : null} +
+ ); +} + const LazyCommandBar = lazy(async () => { const module = await import('./CommandBar'); return { default: module.CommandBar }; @@ -107,6 +167,9 @@ export interface SnapshotsPanelProps { onRestoreSnapshot: (snapshot: FlowSnapshot) => void; onDeleteSnapshot: (id: string) => void; onCompareSnapshot?: (snapshot: FlowSnapshot) => void; + historyPastCount: number; + historyFutureCount: number; + onScrubHistoryTo: (index: number) => void; } export interface PropertiesRailProps { @@ -212,73 +275,19 @@ export function FlowEditorPanels({ Boolean( properties.selectedNode || properties.selectedEdge || properties.selectedNodes.length > 1 ); - const showStudioRail = editorMode === 'studio'; - const railContent = showStudioRail ? ( - - - - ) : showPropertiesRail ? ( - - - - ) : null; return ( <> - + + } + > {commandBar.isOpen ? ( }> {isHistoryOpen ? ( - - - + + } + > + }> + + + ) : null} - - {railContent ? {railContent} : null} - + {editorMode === 'studio' ? ( + + + + } + > + + }> + + + + + ) : null} + + {showPropertiesRail ? ( + + + + } + > + + }> + + + + + ) : null} ); } diff --git a/src/components/FlowTabs.test.tsx b/src/components/FlowTabs.test.tsx new file mode 100644 index 0000000..8ac86b6 --- /dev/null +++ b/src/components/FlowTabs.test.tsx @@ -0,0 +1,51 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { FlowTabs } from './FlowTabs'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (_key: string, fallback?: string) => fallback ?? _key, + }), +})); + +describe('FlowTabs', () => { + function createProps() { + return { + pages: [ + { id: 'page-1', name: 'Page One', nodes: [], edges: [], history: { past: [], future: [] } }, + { id: 'page-2', name: 'Page Two', nodes: [], edges: [], history: { past: [], future: [] } }, + { id: 'page-3', name: 'Page Three', nodes: [], edges: [], history: { past: [], future: [] } }, + ], + activePageId: 'page-1', + onSwitchPage: vi.fn(), + onAddPage: vi.fn(), + onClosePage: vi.fn(), + onRenamePage: vi.fn(), + onReorderPage: vi.fn(), + }; + } + + it('reorders pages when a tab is dropped onto another tab', () => { + const props = createProps(); + render(); + + const tabs = screen.getAllByTestId('flow-page-tab'); + fireEvent.dragStart(tabs[0]); + fireEvent.dragOver(tabs[2]); + fireEvent.drop(tabs[2]); + + expect(props.onReorderPage).toHaveBeenCalledWith('page-1', 'page-3'); + }); + + it('does not reorder when a tab is dropped onto itself', () => { + const props = createProps(); + render(); + + const tabs = screen.getAllByTestId('flow-page-tab'); + fireEvent.dragStart(tabs[1]); + fireEvent.dragOver(tabs[1]); + fireEvent.drop(tabs[1]); + + expect(props.onReorderPage).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/FlowTabs.tsx b/src/components/FlowTabs.tsx index 2270e6c..90c375e 100644 --- a/src/components/FlowTabs.tsx +++ b/src/components/FlowTabs.tsx @@ -12,6 +12,7 @@ interface FlowTabsProps { onAddPage: () => void; onClosePage: (pageId: string) => void; onRenamePage: (pageId: string, newName: string) => void; + onReorderPage: (draggedPageId: string, targetPageId: string) => void; } export const FlowTabs: React.FC = ({ @@ -21,11 +22,14 @@ export const FlowTabs: React.FC = ({ onAddPage, onClosePage, onRenamePage, + onReorderPage, }) => { const { t } = useTranslation(); const isBeveled = IS_BEVELED; const [editingTabId, setEditingTabId] = useState(null); const [editName, setEditName] = useState(''); + const [draggedPageId, setDraggedPageId] = useState(null); + const [dropTargetPageId, setDropTargetPageId] = useState(null); const activeTabClassName = `${getSegmentedTabButtonClass(true, 'sm')} h-10 sm:h-9 border-[var(--brand-primary-200)] bg-[var(--brand-primary-50)] text-[var(--brand-primary-700)]`; const inactiveTabClassName = `${getSegmentedTabButtonClass(false, 'sm')} h-10 sm:h-9 border-[var(--color-brand-border)] bg-[var(--brand-surface)] text-[var(--brand-secondary)] hover:border-[var(--color-brand-border)] hover:bg-[var(--brand-background)] hover:text-[var(--brand-text)]`; @@ -51,6 +55,18 @@ export const FlowTabs: React.FC = ({ } }; + const handleDrop = (targetPageId: string): void => { + if (!draggedPageId || draggedPageId === targetPageId) { + setDraggedPageId(null); + setDropTargetPageId(null); + return; + } + + onReorderPage(draggedPageId, targetPageId); + setDraggedPageId(null); + setDropTargetPageId(null); + }; + return (
= ({ aria-selected={activePageId === page.id} className={` group relative flex items-center gap-2 cursor-pointer select-none transition-all + ${dropTargetPageId === page.id && draggedPageId !== page.id ? 'ring-2 ring-[var(--brand-primary-300)] ring-offset-1 ring-offset-transparent' : ''} ${activePageId === page.id ? activeTabClassName : inactiveTabClassName} `} + draggable={editingTabId !== page.id} + onDragStart={() => setDraggedPageId(page.id)} + onDragOver={(event) => { + if (!draggedPageId || draggedPageId === page.id) { + return; + } + event.preventDefault(); + setDropTargetPageId(page.id); + }} + onDragLeave={() => { + if (dropTargetPageId === page.id) { + setDropTargetPageId(null); + } + }} + onDrop={(event) => { + event.preventDefault(); + handleDrop(page.id); + }} + onDragEnd={() => { + setDraggedPageId(null); + setDropTargetPageId(null); + }} onClick={() => onSwitchPage(page.id)} onDoubleClick={() => handleStartEdit(page)} onKeyDown={(e) => { diff --git a/src/components/ImportRecoveryDialog.test.tsx b/src/components/ImportRecoveryDialog.test.tsx new file mode 100644 index 0000000..c7a0d1e --- /dev/null +++ b/src/components/ImportRecoveryDialog.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ImportRecoveryDialog } from './ImportRecoveryDialog'; +import type { ImportFidelityReport } from '@/services/importFidelity'; + +function createReport(): ImportFidelityReport { + return { + id: 'import-1', + source: 'json', + timestamp: '2026-03-30T00:00:00.000Z', + status: 'failed', + nodeCount: 0, + edgeCount: 0, + elapsedMs: 42, + issues: [ + { + code: 'DOC-001', + severity: 'error', + message: 'Invalid flow file: missing nodes or edges arrays.', + hint: 'Re-export the file from OpenFlowKit or supply both arrays.', + }, + { + code: 'FMT-001', + severity: 'error', + message: 'Unexpected metadata block.', + line: 12, + snippet: '"metadata": { "broken": true }', + }, + ], + summary: { + warningCount: 0, + errorCount: 2, + }, + }; +} + +describe('ImportRecoveryDialog', () => { + it('renders import issues and invokes retry/close actions', () => { + const onRetry = vi.fn(); + const onClose = vi.fn(); + + render( + + ); + + expect(screen.getByRole('dialog', { name: 'Import needs attention' })).toBeTruthy(); + expect(screen.getByText(/broken-flow\.json could not be loaded cleanly/i)).toBeTruthy(); + expect(screen.getByText(/missing nodes or edges arrays/i)).toBeTruthy(); + expect(screen.getByText(/Unexpected metadata block/i)).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: /Try another file/i })); + expect(onRetry).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByRole('button', { name: /Dismiss/i })); + expect(onClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/ImportRecoveryDialog.tsx b/src/components/ImportRecoveryDialog.tsx new file mode 100644 index 0000000..f898b36 --- /dev/null +++ b/src/components/ImportRecoveryDialog.tsx @@ -0,0 +1,151 @@ +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 { MODAL_PANEL_CLASS, SECTION_CARD_CLASS, SECTION_SURFACE_CLASS, STATUS_SURFACE_CLASS } from '@/lib/designTokens'; +import { Button } from './ui/Button'; + +interface ImportRecoveryDialogProps { + fileName: string; + report: ImportFidelityReport; + onRetry: () => void; + onClose: () => void; +} + +function formatSourceLabel(source: ImportFidelityReport['source']): string { + if (source === 'openflowdsl') { + return 'OpenFlow DSL'; + } + + return source.toUpperCase(); +} + +export function ImportRecoveryDialog({ + fileName, + report, + onRetry, + onClose, +}: 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); + + useEffect(() => { + closeButtonRef.current?.focus(); + + function handleEscape(event: KeyboardEvent): void { + if (event.key === 'Escape') { + onClose(); + } + } + + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [onClose]); + + return createPortal( +
+
+
+
+
+ +
+
+

+ Import needs attention +

+

+ {fileName} could not be loaded cleanly. Review the issues below, then retry with a corrected file or another export. +

+
+
+ +
+ +
+
+
+
Source
+
{formatSourceLabel(report.source)}
+
+
+
Errors
+
{report.summary.errorCount}
+
+
+
Warnings
+
{report.summary.warningCount}
+
+
+ +
+
+ + Top recovery issues +
+
+ {visibleIssues.map((issue, index) => ( +
+
+ {typeof issue.line === 'number' ? `Line ${issue.line}: ` : ''} + {issue.message} +
+ {issue.snippet ? ( +
+                      {issue.snippet}
+                    
+ ) : null} + {issue.hint ? ( +

{issue.hint}

+ ) : null} +
+ ))} + {remainingIssueCount > 0 ? ( +

+ {remainingIssueCount} more issue{remainingIssueCount === 1 ? '' : 's'} were captured in the latest import report. +

+ ) : null} +
+
+ +
+ If this file came from another tool, try exporting a plain JSON/OpenFlowKit file again or remove unsupported metadata before retrying. +
+ +
+ + +
+
+
+ +
, + document.body + ); +} diff --git a/src/components/NavigationControls.tsx b/src/components/NavigationControls.tsx index f4f3aac..331342c 100644 --- a/src/components/NavigationControls.tsx +++ b/src/components/NavigationControls.tsx @@ -6,57 +6,48 @@ import { useTranslation } from 'react-i18next'; import { useShortcutHelpActions } from '@/store/viewHooks'; const controlButtonClassName = - 'flex min-h-10 min-w-10 items-center justify-center rounded-[var(--radius-sm)] p-2 text-[var(--brand-secondary)] transition-all hover:bg-[var(--brand-background)] hover:text-[var(--brand-text)] active:scale-95 sm:min-h-9 sm:min-w-9'; + 'flex min-h-10 min-w-10 items-center justify-center rounded-[var(--radius-sm)] p-2 text-[var(--brand-secondary)] transition-all hover:bg-[var(--brand-background)] hover:text-[var(--brand-text)] active:scale-95 sm:min-h-9 sm:min-w-9'; export function NavigationControls(): React.ReactElement { - const { t } = useTranslation(); - const { zoomIn, zoomOut, fitView } = useReactFlow(); - const { zoom } = useViewport(); - const { setShortcutsHelpOpen } = useShortcutHelpActions(); + const { t } = useTranslation(); + const { zoomIn, zoomOut, fitView } = useReactFlow(); + const { zoom } = useViewport(); + const { setShortcutsHelpOpen } = useShortcutHelpActions(); - return ( -
-
- - - + return ( +
+
+ + + -
- {Math.round(zoom * 100)}% -
- - - - -
- - - -
- - - -
+
+ {Math.round(zoom * 100)}%
- ); + + + + +
+ + + +
+ + + +
+
+ ); } diff --git a/src/components/SettingsModal/AISettings.tsx b/src/components/SettingsModal/AISettings.tsx index 14a61a3..1482ac7 100644 --- a/src/components/SettingsModal/AISettings.tsx +++ b/src/components/SettingsModal/AISettings.tsx @@ -233,6 +233,28 @@ export function AISettings(): React.ReactElement { )}
+ {/* Temperature */} +
+
+ + + {(aiSettings.temperature ?? 0.2).toFixed(1)} + +
+ setAISettings({ temperature: parseFloat(e.target.value) })} + className="w-full h-1.5 rounded-full appearance-none cursor-pointer bg-[var(--brand-background)] accent-[var(--brand-primary)]" + /> +

+ Lower = more precise and consistent. Higher = more creative and varied. Default: 0.2 +

+
+ {/* API Key */}
diff --git a/src/components/ShareEmbedModal.tsx b/src/components/ShareEmbedModal.tsx index 718c485..ce7f1a0 100644 --- a/src/components/ShareEmbedModal.tsx +++ b/src/components/ShareEmbedModal.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useState } from 'react'; import { Check, Code2, Copy, ExternalLink, FileCode2, Link, X } from 'lucide-react'; +import { MODAL_PANEL_CLASS, SECTION_CARD_CLASS } from '@/lib/designTokens'; interface ShareEmbedModalProps { viewerUrl: string; @@ -16,7 +17,7 @@ function CopyRow({ label, value, icon: Icon }: { label: string; value: string; i }, [value]); return ( -
+
@@ -63,7 +64,7 @@ export function ShareEmbedModal({ viewerUrl, onClose }: ShareEmbedModalProps): R aria-modal="true" aria-labelledby="share-embed-title" aria-describedby="share-embed-description" - className="relative mx-4 w-full max-w-md rounded-[var(--radius-xl)] border border-[var(--color-brand-border)] bg-[var(--brand-surface)] text-[var(--brand-text)] shadow-[var(--shadow-overlay)] ring-1 ring-black/5 animate-in fade-in zoom-in-95 duration-150" + className={`relative mx-4 w-full max-w-md text-[var(--brand-text)] animate-in fade-in zoom-in-95 duration-150 ${MODAL_PANEL_CLASS}`} onClick={(e) => e.stopPropagation()} >
diff --git a/src/components/SnapshotsPanel.test.tsx b/src/components/SnapshotsPanel.test.tsx new file mode 100644 index 0000000..305e796 --- /dev/null +++ b/src/components/SnapshotsPanel.test.tsx @@ -0,0 +1,40 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { SnapshotsPanel } from './SnapshotsPanel'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (_key: string, fallbackOrOptions?: string | Record) => + typeof fallbackOrOptions === 'string' ? fallbackOrOptions : _key, + }), +})); + +describe('SnapshotsPanel', () => { + it('renders a history scrubber and scrubs to the requested step', () => { + const onScrubHistoryTo = vi.fn(); + + render( + + ); + + fireEvent.change( + screen.getByRole('slider', { name: 'Scrub through recent undo history' }), + { target: { value: '1' } } + ); + + expect(screen.getByText('Undo Timeline')).toBeInTheDocument(); + expect(onScrubHistoryTo).toHaveBeenCalledWith(1); + }); +}); diff --git a/src/components/SnapshotsPanel.tsx b/src/components/SnapshotsPanel.tsx index 8a39531..1224ab4 100644 --- a/src/components/SnapshotsPanel.tsx +++ b/src/components/SnapshotsPanel.tsx @@ -13,6 +13,9 @@ interface SnapshotsPanelProps { onRestoreSnapshot: (snapshot: FlowSnapshot) => void; onDeleteSnapshot: (id: string) => void; onCompareSnapshot?: (snapshot: FlowSnapshot) => void; + historyPastCount: number; + historyFutureCount: number; + onScrubHistoryTo: (index: number) => void; } interface SnapshotCardListProps { @@ -102,6 +105,9 @@ export const SnapshotsPanel: React.FC = ({ onRestoreSnapshot, onDeleteSnapshot, onCompareSnapshot, + historyPastCount, + historyFutureCount, + onScrubHistoryTo, }) => { const { t } = useTranslation(); const [newSnapshotName, setNewSnapshotName] = useState(''); @@ -109,6 +115,8 @@ export const SnapshotsPanel: React.FC = ({ const deleteVersionTitle = t('snapshotsPanel.deleteVersion'); const nodesLabel = (count: number): string => t('snapshotsPanel.nodes', { count }); const edgesLabel = (count: number): string => t('snapshotsPanel.edges', { count }); + const historyTotalSteps = historyPastCount + historyFutureCount + 1; + const historyCurrentIndex = historyPastCount; if (!isOpen) return null; @@ -156,6 +164,36 @@ export const SnapshotsPanel: React.FC = ({
+
+
+

+ {t('snapshotsPanel.undoTimeline', 'Undo Timeline')} +

+

+ {historyCurrentIndex === 0 + ? t('snapshotsPanel.undoTimelineAtEarliest', 'You are at the earliest captured state.') + : historyCurrentIndex === historyTotalSteps - 1 + ? t('snapshotsPanel.undoTimelineAtLatest', 'You are at the latest state.') + : t('snapshotsPanel.undoTimelinePosition', 'Step {{current}} of {{total}} in the current history stack.', { + current: historyCurrentIndex + 1, + total: historyTotalSteps, + })} +

+
+ onScrubHistoryTo(Number(event.currentTarget.value))} + aria-label={t('snapshotsPanel.undoTimelineScrubber', 'Scrub through recent undo history')} + className="w-full accent-[var(--brand-primary)]" + /> +
+ {historyPastCount} undo step{historyPastCount === 1 ? '' : 's'} + {historyFutureCount} redo step{historyFutureCount === 1 ? '' : 's'} +
+
{snapshots.length === 0 ? (

{t('snapshotsPanel.noSnapshots')}

diff --git a/src/components/StudioAIPanel.test.tsx b/src/components/StudioAIPanel.test.tsx index f651ba0..bff0226 100644 --- a/src/components/StudioAIPanel.test.tsx +++ b/src/components/StudioAIPanel.test.tsx @@ -4,6 +4,8 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { StudioAIPanel } from './StudioAIPanel'; +const handleGenerateMock = vi.fn(); + vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: ( @@ -28,7 +30,7 @@ vi.mock('./command-bar/useAIViewState', () => ({ setSelectedImage: vi.fn(), fileInputRef: createRef(), scrollRef: createRef(), - handleGenerate: vi.fn(), + handleGenerate: handleGenerateMock, handleKeyDown: vi.fn(), handleImageSelect: vi.fn(), }), @@ -36,6 +38,7 @@ vi.mock('./command-bar/useAIViewState', () => ({ describe('StudioAIPanel', () => { it('shows the settings CTA when ai is unavailable', () => { + handleGenerateMock.mockReset(); render( { expect(onClearError).toHaveBeenCalledTimes(1); }); + it('shows AI recovery actions for request failures', () => { + handleGenerateMock.mockReset(); + const onClearError = vi.fn(); + + render( + + ); + + expect(screen.getByText('Last request failed')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Retry request' })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: 'Retry request' })); + expect(handleGenerateMock).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByRole('button', { name: 'Dismiss AI error' })); + expect(onClearError).toHaveBeenCalledTimes(1); + }); + + it('offers AI settings recovery for setup-style failures', () => { + const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent'); + + render( + + ); + + expect(screen.getByRole('button', { name: 'Review AI settings' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Review AI settings' })); + + expect(dispatchEventSpy).toHaveBeenCalledTimes(1); + expect((dispatchEventSpy.mock.calls[0]?.[0] as CustomEvent).type).toBe('open-ai-settings'); + + dispatchEventSpy.mockRestore(); + }); + it('opens settings from the empty-state CTA', () => { const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent'); diff --git a/src/components/StudioAIPanel.tsx b/src/components/StudioAIPanel.tsx index 424e2c1..3292cbf 100644 --- a/src/components/StudioAIPanel.tsx +++ b/src/components/StudioAIPanel.tsx @@ -1,33 +1,25 @@ import { useEffect, useState, type ReactElement } from 'react'; import { ArrowUp, - Loader2, - Paperclip, - Square, - Trash2, - WandSparkles, - X, - Crosshair, Edit3, - CheckCircle2, - Plus, - Minus, - RefreshCw, - Key, - Info, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { FLOWPILOT_NAME, IS_BEVELED } from '@/lib/brand'; +import { IS_BEVELED } from '@/lib/brand'; import type { ChatMessage } from '@/services/aiService'; import type { ImportDiff } from '@/hooks/useAIGeneration'; import type { AIReadinessState } from '@/hooks/ai-generation/readiness'; import { useAIViewState } from './command-bar/useAIViewState'; -import { Tooltip } from './Tooltip'; import { EMPTY_CANVAS_EXAMPLES, ITERATION_EXAMPLES, EXAMPLE_ICON_COLORS, } from './studioAiPanelExamples'; +import { + type AIGenerationMode, + ChatHistoryView, + ComposerSection, + PendingDiffBanner, +} from './StudioAIPanelSections'; function getExampleIconColor(index: number): string { return EXAMPLE_ICON_COLORS[index % EXAMPLE_ICON_COLORS.length]; @@ -53,9 +45,6 @@ interface StudioAIPanelProps { onInitialPromptConsumed?: () => void; } -type AIGenerationMode = 'edit' | 'create'; -type ChatBubbleTone = ChatMessage['role']; - function buildGenerationPrompt(prompt: string, mode: AIGenerationMode, nodeCount: number): string { if (nodeCount === 0 || mode === 'edit') { return prompt; @@ -122,14 +111,6 @@ function getInfoIconClassName(isActive: boolean): string { return `h-3.5 w-3.5 focus:outline-none ${isActive ? 'text-orange-400' : 'text-[var(--brand-secondary)]'}`; } -function getChatBubbleClassName(role: ChatBubbleTone): string { - if (role === 'user') { - return 'rounded-br-sm bg-[var(--brand-primary)] text-white shadow-sm'; - } - - return 'rounded-bl-sm border border-[var(--color-brand-border)]/70 bg-[var(--brand-surface)] text-[var(--brand-text)] shadow-sm'; -} - export function StudioAIPanel({ onAIGenerate, isGenerating, @@ -217,314 +198,73 @@ export function StudioAIPanel({ return (
- {pendingDiff && ( -
-
- -

- {pendingDiff.previewTitle} -

-
- {pendingDiff.previewDetail ? ( -

- {pendingDiff.previewDetail} -

- ) : null} - {pendingDiff.previewStats && pendingDiff.previewStats.length > 0 ? ( -
- {pendingDiff.previewStats.map((stat) => ( - - {stat} - - ))} -
- ) : null} -
- {pendingDiff.addedCount > 0 && ( - - - {t('commandBar.aiStudio.addedCount', { - count: pendingDiff.addedCount, - defaultValue: '{{count}} added', - })} - - )} - {pendingDiff.updatedCount > 0 && ( - - - {t('commandBar.aiStudio.updatedCount', { - count: pendingDiff.updatedCount, - defaultValue: '{{count}} updated', - })} - - )} - {pendingDiff.removedCount > 0 && ( - - - {t('commandBar.aiStudio.removedCount', { - count: pendingDiff.removedCount, - defaultValue: '{{count}} removed', - })} - - )} -
-
- - -
-
- )} - {hasHistory && ( -
- -
- )} - -
- {!hasHistory ? ( -
-
- -
-

- {FLOWPILOT_NAME} -

-

- {isCanvasEmpty - ? t('commandBar.aiStudio.emptyDescription', { - appName: FLOWPILOT_NAME, - defaultValue: - 'Describe the diagram you want and {{appName}} will draft the first graph for you.', - }) - : t('commandBar.aiStudio.editDescription', { - appName: FLOWPILOT_NAME, - defaultValue: - 'Describe the changes you want and {{appName}} will update the graph for you.', - })} -

- - {aiReadiness.canGenerate && ( -
- {examplePrompts.map((skill, index) => { - const Icon = skill.icon; - return ( - - ); - })} -
- )} - {!aiReadiness.canGenerate && ( -
- -
- )} -
- ) : ( - <> - {chatMessages.map((msg, idx) => ( -
-
- {msg.parts.map((part, index) => ( -
- {part.text} -
- ))} -
-
- ))} - {isGenerating && ( -
-
- {streamingText ? ( - {streamingText} - ) : ( - - - {retryCount > 0 - ? t('commandBar.aiStudio.retrying', { - retryCount, - defaultValue: 'Retrying ({{retryCount}} of 3)...', - }) - : t('commandBar.aiStudio.generating', 'Generating...')} - - )} -
-
- )} - - )} -
-
- {nodeCount > 0 ? ( -
- - -
- ) : null} - - {selectedNodeCount > 0 && effectiveGenerationMode === 'edit' ? ( -
- - - {t('commandBar.aiStudio.editingSelectedNodes', { - count: selectedNodeCount, - defaultValue: 'Editing {{count}} selected node', - })} - -
- ) : null} - - {selectedImage && ( -
- {t('commandBar.aiStudio.uploadPreviewAlt', - -
+ {pendingDiff ? ( + + ) : null} + { + setPrompt(examplePrompt); + void submitPrompt(examplePrompt); + }} + onOpenAISettings={openAISettings} + onClearChat={onClearChat} + scrollRef={scrollRef} + t={t} + /> + -