From a5ed799d58977d09d8e9676ad986cbf80815c8dd Mon Sep 17 00:00:00 2001 From: Varun Date: Mon, 30 Mar 2026 19:05:29 +0530 Subject: [PATCH] Commit remaining branch changes --- ARCHITECTURE.md | 28 +- README.md | 87 +- docs-site/.astro/content.d.ts | 534 +++++++- .../src/content/docs/collaboration-sharing.md | 25 + docs-site/src/content/docs/context-menu.md | 91 ++ docs-site/src/content/docs/diagram-diff.md | 15 + .../src/content/docs/figma-design-import.md | 9 +- docs-site/src/content/docs/infra-sync.md | 16 +- .../src/content/docs/mobile-experience.md | 83 ++ docs-site/src/content/docs/roadmap.md | 19 + docs-site/src/content/docs/settings.md | 66 + .../src/content/docs/snapshots-recovery.md | 79 ++ docs/internal/COMPETITIVE_ANALYSIS.md | 273 ---- scripts/analyze-bundle.mjs | 191 +++ scripts/translate-all.mjs | 443 +++++++ scripts/translate-remaining.mjs | 1147 +++++++++++++++++ src/App.tsx | 28 +- src/app/routeState.test.ts | 3 +- src/app/routeState.ts | 4 - src/components/CommandBar.test.tsx | 30 +- src/components/ConnectMenu.test.tsx | 23 +- src/components/ConnectMenu.tsx | 98 +- src/components/ConnectMenuSections.tsx | 129 ++ src/components/ContextMenu.test.tsx | 26 +- src/components/ContextMenu.tsx | 30 + src/components/CustomConnectionLine.test.tsx | 52 + src/components/CustomConnectionLine.tsx | 46 +- .../CustomNode.handleInteraction.test.tsx | 22 + src/components/CustomNode.tsx | 121 +- src/components/CustomNodeContent.tsx | 157 +++ src/components/ExportMenuPanel.tsx | 12 +- src/components/FlowCanvas.tsx | 160 +-- src/components/FlowEditor.tsx | 12 + src/components/FlowEditorEmptyState.tsx | 19 +- src/components/FlowEditorPanels.test.tsx | 87 +- src/components/FlowEditorPanels.tsx | 274 ++-- src/components/FlowTabs.test.tsx | 51 + src/components/FlowTabs.tsx | 39 + src/components/ImportRecoveryDialog.test.tsx | 63 + src/components/ImportRecoveryDialog.tsx | 151 +++ src/components/NavigationControls.tsx | 87 +- src/components/SettingsModal/AISettings.tsx | 22 + src/components/ShareEmbedModal.tsx | 5 +- src/components/SnapshotsPanel.test.tsx | 40 + src/components/SnapshotsPanel.tsx | 38 + src/components/StudioAIPanel.test.tsx | 83 +- src/components/StudioAIPanel.tsx | 406 ++---- src/components/StudioAIPanelSections.tsx | 536 ++++++++ src/components/TopNav.tsx | 3 + src/components/WelcomeModal.tsx | 71 +- .../command-bar/DiagramMiniPreview.tsx | 114 ++ .../command-bar/ImportSurfacePrimitives.tsx | 24 +- .../command-bar/ImportViewPanels.tsx | 30 +- src/components/command-bar/RootView.tsx | 19 +- .../command-bar/importDetection.test.ts | 25 + .../command-bar/mermaidImportParser.ts | 23 + .../flow-canvas/FlowCanvasSurface.test.tsx | 128 ++ .../flow-canvas/FlowCanvasSurface.tsx | 244 ++++ .../flow-canvas/StreamingOverlay.tsx | 51 + .../flow-canvas/useFlowCanvasPaste.ts | 8 +- .../flow-editor/FlowEditorChrome.tsx | 2 + .../buildFlowEditorControllerParams.ts | 1 + src/components/flow-editor/chromePropTypes.ts | 1 + .../flow-editor/flowEditorChromeProps.ts | 5 + src/components/flow-editor/panelProps.test.ts | 3 + src/components/flow-editor/panelProps.ts | 9 + .../flow-editor/useFlowEditorController.ts | 1 + .../useFlowEditorInteractionBindings.ts | 12 + .../useFlowEditorScreenBehavior.ts | 12 +- .../flow-editor/useFlowEditorScreenModel.ts | 10 + .../flow-editor/useFlowEditorScreenState.ts | 3 +- src/components/home/GithubCard.tsx | 59 + src/components/home/HomeSidebar.tsx | 145 ++- .../DiagramNodePropertiesRouter.tsx | 4 +- .../useStudioCodePanelController.ts | 376 +++--- src/components/ui/ToastContext.tsx | 6 +- src/components/useExportMenu.test.tsx | 41 +- src/components/useExportMenu.ts | 255 ++-- src/constants.ts | 3 - src/diagram-types/bootstrap.test.ts | 40 + src/diagram-types/bootstrap.ts | 28 + src/diagram-types/builtInPlugins.ts | 20 + src/diagram-types/builtInPropertyPanels.ts | 22 + src/diagram-types/core/propertyPanels.test.ts | 9 +- src/diagram-types/core/propertyPanels.ts | 4 + src/diagram-types/core/registry.ts | 3 + .../registerBuiltInPlugins.test.ts | 15 +- src/diagram-types/registerBuiltInPlugins.ts | 25 +- .../registerBuiltInPropertyPanels.test.ts | 12 +- .../registerBuiltInPropertyPanels.ts | 21 +- src/docs/publicDocsCatalog.js | 189 +-- .../ai-generation/chatHistoryStorage.test.ts | 12 + src/hooks/ai-generation/chatHistoryStorage.ts | 4 +- src/hooks/ai-generation/graphComposer.ts | 4 +- src/hooks/ai-generation/requestLifecycle.ts | 51 +- src/hooks/ai-generation/sqlToErd.ts | 52 +- src/hooks/ai-generation/streamingParser.ts | 79 ++ src/hooks/ai-generation/streamingStore.ts | 65 + .../exportHandlers.test.ts | 17 + .../flow-editor-actions/exportHandlers.ts | 23 +- .../diagramDocumentTransfer.test.ts | 25 + .../flow-export/diagramDocumentTransfer.ts | 5 + src/hooks/mindmapTopicActionRequest.ts | 4 +- src/hooks/nodeLabelEditRequest.ts | 4 +- src/hooks/nodeQuickCreateRequest.ts | 4 +- src/hooks/useAIGeneration.ts | 17 +- src/hooks/useClipboardOperations.ts | 4 +- src/hooks/useFlowEditorActions.test.ts | 27 +- src/hooks/useFlowEditorActions.ts | 7 +- src/hooks/useFlowEditorCallbacks.ts | 8 + src/hooks/useFlowEditorCollaboration.test.ts | 13 + src/hooks/useFlowEditorCollaboration.ts | 5 +- src/hooks/useFlowExport.ts | 21 +- src/hooks/useFlowHistory.ts | 21 + src/hooks/useGithubStars.ts | 67 + src/hooks/useKeyboardShortcuts.test.ts | 22 + src/hooks/useKeyboardShortcuts.ts | 28 +- src/hooks/useMenuKeyboardNavigation.ts | 81 ++ src/hooks/useStyleClipboard.test.ts | 11 + src/hooks/useStyleClipboard.ts | 45 +- src/i18n/usedLocaleCoverage.test.ts | 22 + src/index.css | 41 + src/index.tsx | 13 +- src/lib/colorUtils.ts | 4 +- src/lib/date.ts | 3 + src/lib/designTokens.ts | 22 + src/lib/fuzzyMatch.ts | 16 +- src/lib/legacyBranding.ts | 35 +- src/lib/mermaidParser.ts | 402 +++--- src/lib/mermaidParserModel.ts | 121 ++ src/lib/nodeStyleData.test.ts | 37 + src/lib/nodeStyleData.ts | 46 + src/lib/types.ts | 71 +- src/services/aiService.test.ts | 32 + src/services/aiService.ts | 30 +- src/services/aiServiceSchemas.ts | 96 ++ src/services/collaboration/bootstrap.test.ts | 34 + src/services/collaboration/bootstrap.ts | 77 ++ src/services/collaboration/hookUtils.ts | 24 +- .../collaboration/operationLog.test.ts | 56 + src/services/collaboration/operationLog.ts | 99 ++ src/services/collaboration/reducer.test.ts | 23 + src/services/collaboration/reducer.ts | 18 + .../collaboration/runtimeHookUtils.ts | 65 +- src/services/collaboration/schemas.ts | 31 + .../collaboration/storeBridge.test.ts | 24 +- src/services/collaboration/storeBridge.ts | 32 +- .../collaboration/yjsPeerTransport.test.ts | 41 + .../collaboration/yjsPeerTransport.ts | 27 +- src/services/diagramDocument.ts | 15 +- src/services/diagramDocumentSchemas.ts | 16 + src/services/elkLayout.ts | 23 + src/services/geminiService.ts | 294 +---- src/services/geminiSystemInstruction.ts | 314 +++++ src/services/githubFetcher.ts | 2 +- src/services/mermaid/parseMermaidByType.ts | 4 +- src/services/onboarding/events.test.ts | 36 +- src/services/storage/flowDocumentModel.ts | 5 +- .../storage/flowPersistStorage.test.ts | 2 + src/services/storage/flowPersistStorage.ts | 44 +- src/services/storage/indexedDbHelpers.ts | 31 + src/services/storage/indexedDbSchema.test.ts | 64 +- src/services/storage/indexedDbSchema.ts | 91 +- .../storage/indexedDbStateStorage.test.ts | 46 +- src/services/storage/indexedDbStateStorage.ts | 44 +- src/services/storage/localFirstRepository.ts | 50 +- src/services/storage/localFirstRuntime.ts | 35 +- .../storage/persistedDocumentAdapters.test.ts | 2 +- .../storage/persistedDocumentAdapters.ts | 42 +- src/services/storage/persistenceTypes.ts | 2 +- src/services/storage/snapshotStorage.ts | 14 +- src/services/storage/storageRuntime.test.ts | 40 + src/services/storage/storageRuntime.ts | 48 + src/services/storage/storageSchemas.test.ts | 66 + src/services/storage/storageSchemas.ts | 69 + src/services/storage/storageTelemetry.ts | 2 +- src/store.test.ts | 4 +- src/store.ts | 49 +- src/store/actionFactory.ts | 7 + .../actions/createAIAndSelectionActions.ts | 3 +- src/store/actions/createCanvasActions.ts | 6 +- .../actions/createDesignSystemActions.ts | 3 +- .../actions/createHistoryActions.test.ts | 162 +++ src/store/actions/createHistoryActions.ts | 50 +- src/store/actions/createLayerActions.ts | 4 +- src/store/actions/createTabActions.ts | 41 +- src/store/actions/createViewActions.ts | 3 +- .../actions/createWorkspaceDocumentActions.ts | 14 +- src/store/actions/syncTabNodesEdges.ts | 3 +- src/store/aiSettings.test.ts | 41 +- src/store/aiSettings.ts | 3 + src/store/aiSettingsPersistence.ts | 245 +++- src/store/aiSettingsSchemas.test.ts | 57 + src/store/aiSettingsSchemas.ts | 75 ++ src/store/canvasHooks.ts | 27 +- src/store/createFlowStore.test.ts | 55 + src/store/createFlowStore.ts | 18 + src/store/createFlowStorePersistOptions.ts | 22 + src/store/createFlowStoreState.ts | 20 + src/store/designSystemHooks.ts | 38 +- src/store/documentHooks.ts | 37 +- src/store/editorPageHooks.ts | 65 +- src/store/historyHooks.ts | 18 +- src/store/historyState.ts | 8 + src/store/persistence.test.ts | 41 + src/store/persistence.ts | 148 ++- src/store/persistenceSchemas.ts | 78 ++ src/store/selectionHooks.ts | 57 +- src/store/selectors.test.ts | 61 + src/store/selectors.ts | 220 ++++ src/store/slices/createCanvasEditorSlice.ts | 78 ++ src/store/slices/createExperienceSlice.ts | 41 + src/store/slices/createWorkspaceSlice.ts | 36 + src/store/tabHooks.ts | 43 +- src/store/types.ts | 97 ++ src/store/viewHooks.ts | 71 +- src/store/workspaceDocumentModel.ts | 1 + src/theme.ts | 4 +- tsconfig.tsbuildinfo | 2 +- vite.config.ts | 2 +- web/.astro/types.d.ts | 1 + 221 files changed, 10888 insertions(+), 2876 deletions(-) create mode 100644 docs-site/src/content/docs/context-menu.md create mode 100644 docs-site/src/content/docs/mobile-experience.md create mode 100644 docs-site/src/content/docs/settings.md create mode 100644 docs-site/src/content/docs/snapshots-recovery.md delete mode 100644 docs/internal/COMPETITIVE_ANALYSIS.md create mode 100644 scripts/analyze-bundle.mjs create mode 100644 scripts/translate-all.mjs create mode 100644 scripts/translate-remaining.mjs create mode 100644 src/components/ConnectMenuSections.tsx create mode 100644 src/components/CustomConnectionLine.test.tsx create mode 100644 src/components/CustomNodeContent.tsx create mode 100644 src/components/FlowTabs.test.tsx create mode 100644 src/components/ImportRecoveryDialog.test.tsx create mode 100644 src/components/ImportRecoveryDialog.tsx create mode 100644 src/components/SnapshotsPanel.test.tsx create mode 100644 src/components/StudioAIPanelSections.tsx create mode 100644 src/components/command-bar/DiagramMiniPreview.tsx create mode 100644 src/components/command-bar/importDetection.test.ts create mode 100644 src/components/command-bar/mermaidImportParser.ts create mode 100644 src/components/flow-canvas/FlowCanvasSurface.test.tsx create mode 100644 src/components/flow-canvas/FlowCanvasSurface.tsx create mode 100644 src/components/flow-canvas/StreamingOverlay.tsx create mode 100644 src/components/home/GithubCard.tsx create mode 100644 src/diagram-types/bootstrap.test.ts create mode 100644 src/diagram-types/bootstrap.ts create mode 100644 src/diagram-types/builtInPlugins.ts create mode 100644 src/diagram-types/builtInPropertyPanels.ts create mode 100644 src/hooks/ai-generation/streamingParser.ts create mode 100644 src/hooks/ai-generation/streamingStore.ts create mode 100644 src/hooks/useGithubStars.ts create mode 100644 src/hooks/useMenuKeyboardNavigation.ts create mode 100644 src/lib/date.ts create mode 100644 src/lib/designTokens.ts create mode 100644 src/lib/mermaidParserModel.ts create mode 100644 src/lib/nodeStyleData.test.ts create mode 100644 src/lib/nodeStyleData.ts create mode 100644 src/services/aiServiceSchemas.ts create mode 100644 src/services/collaboration/bootstrap.test.ts create mode 100644 src/services/collaboration/bootstrap.ts create mode 100644 src/services/collaboration/operationLog.test.ts create mode 100644 src/services/collaboration/operationLog.ts create mode 100644 src/services/collaboration/schemas.ts create mode 100644 src/services/diagramDocumentSchemas.ts create mode 100644 src/services/geminiSystemInstruction.ts create mode 100644 src/services/storage/storageRuntime.test.ts create mode 100644 src/services/storage/storageRuntime.ts create mode 100644 src/services/storage/storageSchemas.test.ts create mode 100644 src/services/storage/storageSchemas.ts create mode 100644 src/store/actionFactory.ts create mode 100644 src/store/actions/createHistoryActions.test.ts create mode 100644 src/store/aiSettingsSchemas.test.ts create mode 100644 src/store/aiSettingsSchemas.ts create mode 100644 src/store/createFlowStore.test.ts create mode 100644 src/store/createFlowStore.ts create mode 100644 src/store/createFlowStorePersistOptions.ts create mode 100644 src/store/createFlowStoreState.ts create mode 100644 src/store/historyState.ts create mode 100644 src/store/persistenceSchemas.ts create mode 100644 src/store/selectors.test.ts create mode 100644 src/store/selectors.ts create mode 100644 src/store/slices/createCanvasEditorSlice.ts create mode 100644 src/store/slices/createExperienceSlice.ts create mode 100644 src/store/slices/createWorkspaceSlice.ts 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} + /> + -