diff --git a/.commitlintrc.yaml b/.commitlintrc.yaml index 9cb74a7..0974185 100644 --- a/.commitlintrc.yaml +++ b/.commitlintrc.yaml @@ -1,2 +1,2 @@ extends: - - "@commitlint/config-conventional" + - '@commitlint/config-conventional' diff --git a/.cruft.json b/.cruft.json index 48ed88c..3cb6709 100644 --- a/.cruft.json +++ b/.cruft.json @@ -4,9 +4,9 @@ "checkout": "main", "context": { "cookiecutter": { - "project_name": "ui", + "project_name": "studio", "description": "A drag and drop UI for building Temporal workflows", - "author": "Zigflow authors ", + "author": "Zigflow authors ", "type": "svelte", "_template": "https://github.com/mrsimonemms/new", "_commit": "575419a2c0d81d09e68c8a42d027c32b138f1b71" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2912e00..5924cf8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,11 @@ { "image": "ghcr.io/mrsimonemms/devcontainers/full", "name": "devcontainer", - "features": {}, + "features": { + "ghcr.io/dhoeric/features/trivy:1": {}, + "ghcr.io/devcontainers-extra/features/go-task:1": {}, + "ghcr.io/rio/features/skaffold:2": {} + }, "customizations": { "vscode": { "extensions": [ @@ -11,7 +15,15 @@ "settings": {} } }, + "postCreateCommand": { + "playwright": "npx --yes playwright install --with-deps" + }, "containerEnv": { + "PLAYWRIGHT_HTML_HOST": "0.0.0.0", + "PUBLIC_WORKFLOWS_DIR": "/workspaces/studio/workflows", "VITE_HOST": "0.0.0.0" - } + }, + "forwardPorts": [ + 5173 + ] } diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 3897265..0000000 --- a/.eslintignore +++ /dev/null @@ -1,13 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example - -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4d23546 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,387 @@ +name: Build +on: + push: + branches: + - main + tags: + - "v*.*.*" + pull_request: + branches: + - main + workflow_dispatch: +permissions: + actions: read + contents: write + packages: write + id-token: write + pull-requests: read + security-events: write +jobs: + commitlint: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # fetch-depth is required + + - uses: wagoid/commitlint-github-action@v6 + + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # Required for pre-commit to be able scan history + + - uses: actions/setup-node@v6 + with: + node-version: lts/* + + - run: npm ci + + - uses: actions/setup-python@v6 + with: + python-version: 3.x + + - uses: pre-commit/action@v3.0.1 + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: lts/* + + - name: Install dependencies + run: npm ci + + - name: Linting + run: npm run lint + + - name: Checking + run: npm run check + + helm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install Temporal CLI + uses: temporalio/setup-temporal@v0 + + - name: Install Helm + uses: azure/setup-helm@v4 + + - name: helm lint + run: helm lint charts/studio + + - name: Run Trivy scan + uses: aquasecurity/trivy-action@0.35.0 + with: + scan-type: config + scan-ref: ./charts/studio + format: sarif + output: trivy-helm.sarif + exit-code: "1" + severity: HIGH,CRITICAL + limit-severities-for-sarif: true + + - name: Upload Trivy scan results to GitHub Security tab + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: trivy-helm.sarif + + - name: Install Skaffold + run: | + curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 + chmod +x skaffold + sudo mv skaffold /usr/local/bin/ + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create Kind cluster + uses: helm/kind-action@v1 + with: + cluster_name: kind + + - name: Build studio image + uses: docker/build-push-action@v6 + with: + context: . + tags: studio:latest + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Load image into Kind + run: kind load docker-image studio:latest + + - name: Deploy Studio + run: | + echo '{"builds":[{"imageName":"studio","tag":"studio:latest"}]}' > /tmp/artifacts.json + skaffold deploy --build-artifacts /tmp/artifacts.json + + - name: Wait for Studio deployment + run: kubectl rollout status -n zigflow deployment/studio --timeout=30s + + - name: Port forward Studio + run: kubectl port-forward svc/studio -n zigflow 3000:3000 & + + - name: Call webapp + run: | + timeout 30s curl -fsSL localhost:3000 || { + echo "Calling UI timed out or failed" + exit 1 + } + + - name: Dump logs on failure + if: failure() + run: | + kubectl get pods -A + kubectl logs -n zigflow deploy/studio --all-containers=true || true + + # Generate metadata shared across all build jobs + metadata: + runs-on: ubuntu-latest + needs: + - commitlint + - helm + - pre-commit + - test + outputs: + is_tag: ${{ steps.branch-name.outputs.is_tag }} + tag: ${{ steps.branch-name.outputs.tag }} + current_branch: ${{ steps.branch-name.outputs.current_branch }} + version: ${{ steps.metadata.outputs.version }} + commit_id: ${{ steps.metadata.outputs.commit_id }} + git_repo: ${{ steps.metadata.outputs.git_repo }} + push: ${{ steps.metadata.outputs.push }} + platforms: ${{ steps.metadata.outputs.platforms }} + is_prerelease: ${{ steps.metadata.outputs.is_prerelease }} + steps: + - name: Get branch names + id: branch-name + uses: tj-actions/branch-names@v9 + with: + strip_tag_prefix: v + + - name: Generate metadata + id: metadata + run: | + if [ "${{ steps.branch-name.outputs.is_tag }}" = "true" ]; + then + echo "version=${{ steps.branch-name.outputs.tag }}" >> "$GITHUB_OUTPUT" + echo "platforms=[\"linux/amd64\",\"linux/arm64\"]" >> "$GITHUB_OUTPUT" + echo "push=true" >> "$GITHUB_OUTPUT" + + # Detect if tag is a pre-release (contains -rc, -beta, -alpha, etc.) + TAG="${{ steps.branch-name.outputs.tag }}" + if echo "$TAG" | grep -qE -- '-(rc|beta|alpha|pre)'; then + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + fi + else + echo "version=development" >> "$GITHUB_OUTPUT" + echo "platforms=[\"linux/amd64\"]" >> "$GITHUB_OUTPUT" + echo "push=${{ github.ref == 'refs/heads/main' }}" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + fi + + echo "commit_id=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + echo "git_repo=github.com/${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT" + + # Build studio Docker images in parallel by architecture + docker_studio: + runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + needs: metadata + strategy: + matrix: + platform: ${{ fromJson(needs.metadata.outputs.platforms) }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: needs.metadata.outputs.push == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate platform-specific tag + id: tag + run: | + PLATFORM_TAG=$(echo "${{ matrix.platform }}" | sed 's/\//-/g') + echo "platform_tag=${PLATFORM_TAG}" >> "$GITHUB_OUTPUT" + + # Image name with SHA-based tag for this platform + echo "image_ref=ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_SHA}-${PLATFORM_TAG}" >> "$GITHUB_OUTPUT" + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + build-args: | + GIT_COMMIT=${{ needs.metadata.outputs.commit_id }} + GIT_REPO=${{ needs.metadata.outputs.git_repo }} + VERSION=${{ needs.metadata.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: ${{ matrix.platform }} + load: ${{ needs.metadata.outputs.push == 'false' }} + push: ${{ needs.metadata.outputs.push == 'true' }} + tags: ${{ steps.tag.outputs.image_ref }} + # Save as artifact for non-main branches + + - name: Pull images + if: ${{ needs.metadata.outputs.push == 'true' }} + run: docker pull ${{ steps.tag.outputs.image_ref }} + + - name: Run Trivy scan + uses: aquasecurity/trivy-action@0.35.0 + with: + image-ref: ${{ steps.tag.outputs.image_ref }} + format: sarif + output: trivy-zigflow-${{ steps.tag.outputs.platform_tag }}.sarif + exit-code: "1" + ignore-unfixed: true + vuln-type: os,library + severity: HIGH,CRITICAL + limit-severities-for-sarif: true + + - name: Upload Trivy scan results to GitHub Security tab + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: trivy-zigflow-${{ steps.tag.outputs.platform_tag }}.sarif + + # Combine architecture-specific images into a multi-arch manifest + docker_studio_manifest: + runs-on: ubuntu-latest + if: needs.metadata.outputs.push == 'true' + needs: + - metadata + - docker_studio + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Cosign + uses: sigstore/cosign-installer@v4.0.0 + + - name: Install Syft + uses: jaxxstorm/action-install-gh-release@v1.10.0 + with: + repo: anchore/syft + + - name: Generate manifest tags and create manifest + run: | + REPO_LOWER="ghcr.io/${GITHUB_REPOSITORY,,}" + + # Build list of source images based on platforms + SOURCE_IMAGES="${REPO_LOWER}:${GITHUB_SHA}-linux-amd64" + + # Add arm64 only if it was built (tags only) + if [ "${{ needs.metadata.outputs.is_tag }}" = "true" ]; + then + SOURCE_IMAGES="${SOURCE_IMAGES} ${REPO_LOWER}:${GITHUB_SHA}-linux-arm64" + fi + + # Determine target tags based on ref type + if [ "${{ needs.metadata.outputs.is_tag }}" = "true" ]; + then + # For tags: choose latest/prerelease + version tag + SHA based on pre-release status + if [ "${{ needs.metadata.outputs.is_prerelease }}" = "true" ]; then + TARGET_TAGS="${REPO_LOWER}:prerelease ${REPO_LOWER}:${{ needs.metadata.outputs.tag }} ${REPO_LOWER}:${GITHUB_SHA}" + else + TARGET_TAGS="${REPO_LOWER}:latest ${REPO_LOWER}:${{ needs.metadata.outputs.tag }} ${REPO_LOWER}:${GITHUB_SHA}" + fi + elif [ "${{ github.ref }}" = "refs/heads/main" ]; + then + # For main branch: branch-main + SHA + TARGET_TAGS="${REPO_LOWER}:branch-main ${REPO_LOWER}:${GITHUB_SHA}" + else + # For other branches: branch- + SHA + BRANCH="${{ needs.metadata.outputs.current_branch }}" + BRANCH_TAG="branch-${BRANCH//\//-}" + BRANCH_TAG="${BRANCH_TAG,,}" + TARGET_TAGS="${REPO_LOWER}:${BRANCH_TAG} ${REPO_LOWER}:${GITHUB_SHA}" + fi + + # Create manifest for each target tag + for TAG in ${TARGET_TAGS}; do + echo "Creating manifest for ${TAG} from: ${SOURCE_IMAGES}" + docker buildx imagetools create -t "${TAG}" ${SOURCE_IMAGES} + + cosign sign --yes "${TAG}" + syft "${TAG}" -o cyclonedx-json > sbom.json + cosign attach sbom --sbom sbom.json "${TAG}" + cosign sign --yes --attachment sbom "${TAG}" + done + + # Build and publish Helm chart in parallel + helm_chart: + runs-on: ubuntu-latest + needs: metadata + if: needs.metadata.outputs.is_tag == 'true' + steps: + - uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: ">=1.25.0" + + - name: Prepare chart README + run: | + go install github.com/norwoodj/helm-docs/cmd/helm-docs@latest + helm-docs + yq -i '.version = "${{ needs.metadata.outputs.tag }}"' charts/studio/Chart.yaml + yq -i '.annotations."org.opencontainers.image.documentation" = "https://github.com/zigflow/studio/blob/${{ needs.metadata.outputs.tag }}/charts/studio/README.md"' charts/studio/Chart.yaml + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + id: gpg + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + # Helm requires the legacy GPG format + # @link https://helm.sh/docs/topics/provenance/#the-workflow + - name: Convert GPG v2 key + run: | + gpg --export > ~/.gnupg/pubring.gpg + gpg --batch --pinentry-mode loopback --yes --passphrase '${{ secrets.GPG_PASSPHRASE }}' --export-secret-key > ~/.gnupg/secring.gpg + + - name: Publish Helm chart + uses: appany/helm-oci-chart-releaser@v0.5.0 + with: + name: studio + registry: ghcr.io + repository: ${{ github.repository_owner }}/charts + tag: ${{ needs.metadata.outputs.tag }} + registry_username: ${{ github.actor }} + registry_password: ${{ secrets.GITHUB_TOKEN }} + sign: true + signing_key: ${{ steps.gpg.outputs.name }} + signing_passphrase: ${{ secrets.GPG_PASSPHRASE }} + update_dependencies: true diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index c556dcd..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Deploy to GitHub Pages -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: -permissions: - pull-requests: read -jobs: - commitlint: - runs-on: ubuntu-latest - env: - GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # fetch-depth is required - - # This needs a generated dependency - doesn't add anything to this test - - run: rm tsconfig.json - - - uses: wagoid/commitlint-github-action@v5 - - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v4 - with: - python-version: 3.x - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '>=1.20.0' - - - name: Set up JS - uses: actions/setup-node@v4 - with: - node-version: lts/* - cache: "npm" - - - name: Install dependencies - run: npm ci - - - uses: pre-commit/action@v3.0.0 - - build: - runs-on: ubuntu-latest - needs: - - commitlint - - pre-commit - steps: - - name: Checkout - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - - name: Install dependencies - run: npm ci - - - name: Lint - run: npm run lint - - - name: Build - run: | - npm run build - touch build/.nojekyll - - - name: Upload Artifacts - uses: actions/upload-pages-artifact@v3 - with: - path: 'build/' - - deploy: - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - needs: - - build - - permissions: - pages: write - id-token: write - - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - steps: - - name: Deploy - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 96de2d4..07e5b16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,36 @@ -dist -tmp - node_modules -.idea +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS .DS_Store Thumbs.db -.commit -.devbox -.envrc -coverage -.nyc_output -/build -.vercel -/.svelte-kit -/package +# Env .env .env.* !.env.example +!.env.test + +# Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* -.claude +static/fonts + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ + +/workflows +!/workflows/demo-workflow.yaml +.claude/ diff --git a/.licenserc.yaml b/.licenserc.yaml index 43bdf1c..ed3bb74 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -1,17 +1,17 @@ header: license: spdx-id: Apache-2.0 - copyright-owner: Zigflow authors + copyright-owner: Zigflow authors copyright-year: 2025 - 2026 paths-ignore: - - "**/dist" - - "**/tmp" + - '**/dist' + - '**/tmp' - LICENSE - - "**/.*" - - "**/go.*" - - "**/*.{json,md,yml,yaml,txt}" - - "**/.gitkeep" - - "**/*.svelte" + - '**/.*' + - '**/go.*' + - '**/*.{json,md,yml,yaml,txt}' + - '**/.gitkeep' + - '**/*.svelte' comment: on-failure dependency: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf534f5..4e116e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,14 +10,16 @@ repos: args: - --autofix - --no-sort-keys + - --no-ensure-ascii - id: check-json - id: check-yaml + exclude: charts args: - --allow-multiple-documents - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/compilerla/conventional-pre-commit - rev: v4.3.0 + rev: v4.4.0 hooks: - id: conventional-pre-commit stages: @@ -27,22 +29,11 @@ repos: hooks: - id: markdown-toc - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.20.0 + rev: v0.21.0 hooks: - id: markdownlint-cli2 - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.8 - hooks: - - id: prettier - args: - - --plugin - - prettier-plugin-svelte - - --plugin - - prettier-plugin-organize-imports - stages: - - pre-commit - repo: https://github.com/pre-commit/mirrors-eslint - rev: v10.0.0-beta.0 + rev: v10.0.3 hooks: - id: eslint files: \.([jt]sx?|svelte)$ # *.js, *.jsx *.ts, *.tsx and *.svelte @@ -51,3 +42,16 @@ repos: rev: v0.2.4 hooks: - id: scan + - repo: local + hooks: + - id: format + name: Format code + language: unsupported + entry: npm run format + - repo: https://github.com/norwoodj/helm-docs + rev: v1.14.2 + hooks: + - id: helm-docs-built + args: + - --template-files=./charts/zigflow/README.tpl +exclude: charts/.*/README.md diff --git a/.prettierignore b/.prettierignore index 9cbe644..ab65cd7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,17 +1,14 @@ -*.md -*.yml -*.yaml -*.json -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example - -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml +# Package Managers package-lock.json +pnpm-lock.yaml yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ +README.md + +*.yaml +*.json +*.yml diff --git a/.prettierrc b/.prettierrc index a90fb79..7a8403d 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,8 +1,25 @@ { - "useTabs": true, - "singleQuote": true, - "trailingComma": "all", - "printWidth": 80, - "plugins": ["prettier-plugin-svelte", "prettier-plugin-organize-imports"], - "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] + "singleQuote": true, + "trailingComma": "all", + "printWidth": 80, + "plugins": [ + "prettier-plugin-svelte", + "@trivago/prettier-plugin-sort-imports" + ], + "importOrder": ["^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "importOrderParserPlugins": ["typescript", "decorators-legacy"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte", + "importOrder": ["^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "importOrderParserPlugins": ["typescript", "decorators-legacy"] + } + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 9543518..c884e76 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ ".github/workflows/*.{yml,yaml}" ] }, + "prettier.prettierPath": "node_modules/prettier/index.cjs", "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..94499ec --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,534 @@ +# Claude Instructions (zigflow.dev visual editor) + +This repository contains a **visual flow editor for Zigflow**, built using: + +- **SvelteKit** +- **TypeScript** +- **Svelte Flow** +- **App-defined SCSS (no general-purpose CSS framework)** +- **SvelteKit Node adapter** +- **Docker (production runtime)** + +The UI is an implementation detail. +The **Flow Graph model (IR) is the source of truth**. + +Claude should prioritize correctness, clarity, and alignment with existing +tooling over novelty or abstraction. + +--- + +## Core principles + +- Treat the flow graph / IR as authoritative +- UI state must be serializable and reproducible +- Prefer explicit, boring data structures over clever abstractions +- Optimize for correctness and debuggability over minimal code +- Developer experience matters — keep things understandable +- Assume this will run long-lived in Docker + +--- + +## Technology stack (authoritative) + +- **Framework:** SvelteKit +- **Adapter:** `@sveltejs/adapter-node` +- **Language:** TypeScript (required; no new JS-only files) +- **Graph / canvas:** Svelte Flow +- **Styling:** App-defined SCSS (no general-purpose CSS framework) +- **Runtime:** Node.js (Docker) +- **Linting:** ESLint (flat config) +- **Formatting:** Prettier (project-defined config) +- **i18n:** i18next (required) + +Do not: + +- Introduce React or React-specific patterns +- Introduce general-purpose CSS frameworks (Bulma, Tailwind, Bootstrap, etc.) +- Introduce utility-first CSS systems +- Bypass TypeScript with `any` unless unavoidable and documented +- Add alternative formatters or linters + +--- + +## Internationalisation (i18n) requirements (mandatory) + +This project **must** use an established i18n library, not a custom implementation. + +### Library + +- Use **i18next** with a Svelte-compatible integration (do not build our own + i18n layer). +- Keep translations in JSON resource files. +- Prefer **literal Unicode characters** in i18n JSON (e.g. `…`, `£`, `→`) + over HTML entities (e.g. `…`) or escaped codepoints (e.g. `\u2026`) + unless a tool forces escaping. + +### Supported locales (for now) + +- `en` (English, International) — default +- `en-GB` (English, British) + +### Locale selection priority (authoritative) + +1. User-selected locale in the UI (persisted) +2. Browser language headers (e.g. `navigator.languages`) +3. Fallback to `en` + +### Fallback behaviour + +- `en-GB` **must** fall back to `en` for missing keys so British English only + needs to override deltas. +- Do not duplicate `en` strings into `en-GB` unless the wording/spelling + genuinely differs. + +### Hard rule: no new plain text in the UI + +- **Do not introduce new user-visible strings inline** in Svelte components, + stores, routes, or helpers. +- Any new user-visible text **must** be added as an i18n string key and rendered + via `t(...)`. +- This includes: + - Button labels, headings, breadcrumbs, empty states + - Toasts, alerts, errors shown to users + - Inspector labels and field names + - Modal titles and helper text + +Allowed exceptions (narrow): + +- User data (workflow names, task names, branch labels, etc.) +- Debug-only `console.*` output (must not be user-facing) + +### Key naming + structure guidance + +- Use a predictable, boring hierarchy (e.g. `editor.*`, `sidebar.*`, `inspector.*`, + `errors.*`, `actions.*`). +- Prefer stable keys over copy-based keys (do not bake full sentences into key + names). +- Keep strings short and composable where it helps reuse, but do not over-abstract. + +### Testing expectation + +- Any feature work that introduces UI text must include the corresponding `en` key. +- If a string is British-specific, add it to `en-GB`; otherwise only `en` is + required. + +--- + +## Runtime & deployment assumptions + +- The app runs as a **Node server**, not a static site +- Docker is the primary production environment +- File system access must be treated as ephemeral +- All state must be serializable (DB, object storage, or exportable files) + +Do not: + +- Rely on in-memory state for persistence +- Assume serverless constraints +- Introduce adapter-specific hacks + +--- + +## Project structure guidance + +This project uses a small number of **conceptual root directories**. +Their purpose matters more than their exact contents. + +### `src/lib/graph/` + +Authoritative flow graph model and schema. + +- Node and edge types +- Graph structure and invariants +- Validation logic +- No UI code +- Must be usable from non-UI contexts (CLI, tests, exporters) + +### `src/lib/export/` + +Exporters and code generation. + +- Pure functions only +- Accept validated graph models +- No UI dependencies +- Deterministic output + +### `src/lib/ui/` + +Reusable UI primitives and editor-specific components. + +- Svelte components only +- Styling via app-owned SCSS +- May depend on Svelte Flow +- No graph semantics or validation logic + +### `src/routes/` + +SvelteKit routes and server endpoints. + +- Page composition +- Load/save/export endpoints +- No business logic +- No graph validation or mutation logic + +### `src/routes/workflows//` + +Workflow-specific editor routes. + +- Each workflow is addressed by a stable `workflowId` +- Routes under this path are responsible for: + - Loading workflow state + - Rendering the editor UI + - Invoking validation and export logic +- Workflow identity must come from the route, not inferred from UI state + +Do not: + +- Introduce generic `shared`, `common`, or `utils` directories +- Place domain logic under `routes` +- Couple graph logic to Svelte components +- Encode workflow identity implicitly in component state + +### `src/styles/` + +Application-owned SCSS. + +- Design tokens (spacing, colours, typography) +- Layout helpers actually used by the app +- UI component styles +- Svelte Flow-specific styling + +--- + +## Architecture rules + +- Separate concerns strictly: + - **Flow schema & validation** (no UI code) + - **Exporters / code generation** + - **UI (Svelte components, Svelte Flow, inspector panels)** + - **Server routes (load/save/export)** +- Validation must operate on the flow graph, not UI components +- Exported output must never depend on UI-specific state +- UI should adapt to the model, not vice versa + +--- + +## Flow graph model + +- Nodes and edges must have **stable, explicit IDs** +- All node types must be explicitly typed +- No implicit behavior inferred from visual layout or position +- Graph must be fully serializable to JSON +- The graph schema must be usable outside the UI (CLI, tests, exporters) + +Do not: + +- Encode logic in Svelte component state +- Infer control flow from x/y positioning +- Allow invalid graphs to export + +--- + +## Node behavior + +Each node type must define: + +- Allowed incoming edge count +- Allowed outgoing edge count +- Required configuration fields +- Validation rules specific to that node + +Specific rules: + +- Condition nodes must label outgoing edges +- Join nodes must validate fan-in count +- Entry node must be explicit and unique + +--- + +## Validation expectations + +Before export, the following must be enforced: + +- Exactly one entry node +- All nodes reachable from the entry node +- No orphaned edges +- No cycles unless explicitly supported +- Node-specific structural constraints enforced + +Validation errors must be: + +- Deterministic +- Human-readable +- Mapped to node IDs where possible +- Suitable for UI display + +--- + +## UI & styling guidelines + +### Svelte + Svelte Flow + +- Svelte Flow is the canonical canvas abstraction +- Use **Svelte Flow’s native CSS** for graph rendering +- Do not reimplement or override core Svelte Flow layout behavior +- Inspector panels edit **node config**, not graph topology +- UI must reflect validation state clearly +- Avoid auto-magic graph rewrites +- All user-visible UI strings must come from i18n resources via `t(...)` (no + inline copy). + +--- + +### UI primitives & styling approach + +This project does **not** use a general-purpose CSS framework. + +Instead: + +- UI styling is defined explicitly in app-owned SCSS +- Common UI elements are implemented as small, reusable Svelte components +- Styling exists to support correctness and clarity, not visual experimentation + +Core expectations: + +- Define a small set of UI primitives (buttons, inputs, panels, modals) +- Prefer composition over large, configurable components +- Keep styles predictable and easy to audit +- Avoid hidden behaviour encoded in CSS +- Layout should be explicit in Svelte components + +Do not: + +- Recreate a full component framework +- Introduce large sets of generic utility classes +- Encode application logic in CSS +- Add theming or design-token abstractions prematurely + +--- + +### SCSS rules + +- All custom styles must be written in **SCSS** +- Prefer variables, mixins, and nesting over repetition +- SCSS should express structure and intent, not act as a component framework +- Keep Svelte Flow styling separate from app UI styles +- Avoid global CSS leakage where possible + +Encouraged: + +- A small number of well-named SCSS entry points +- Clear separation between: + - Design tokens (spacing, colours, typography) + - Layout helpers actually used by the app + - App-specific component styles + - Svelte Flow-related styles + +--- + +## Performance guidelines (important) + +This editor must remain responsive for **medium-to-large graphs**. +Performance regressions are considered correctness issues. + +### Graph & state management + +- Treat the Flow Graph as immutable at the conceptual level +- Prefer targeted updates over full graph replacement +- Avoid deep cloning unless necessary +- Do not recompute validation on every minor UI interaction +- Debounce or batch expensive operations (validation, export) + +--- + +### Svelte-specific guidance + +- Avoid unnecessary reactive statements on large collections +- Be explicit about reactivity boundaries +- Prefer derived values over duplicated state +- Do not bind large objects directly to form inputs + +--- + +### Deprecated APIs and patterns + +**CRITICAL: Never use deprecated APIs or patterns.** + +This applies to all dependencies and frameworks in the project. + +Core principles: + +- Use current, non-deprecated APIs for all new code +- If a deprecation warning appears, fix it immediately +- Do not ignore, suppress, or work around deprecation warnings +- Follow the official migration path for deprecated features +- Prefer the recommended pattern over backwards-compatible approaches + +When in doubt: + +- Check TypeScript hints and IDE warnings +- Consult official documentation for the relevant library or framework +- Use the pattern that produces no warnings +- Ask + +--- + +### Svelte Flow usage + +- Do not recreate node or edge arrays unnecessarily +- Avoid excessive custom node re-rendering +- Keep custom node components lightweight +- Avoid DOM-heavy node templates +- Be cautious with large numbers of edge labels + +--- + +### CSS & layout performance + +- Avoid expensive global selectors +- Minimize deeply nested SCSS rules +- Do not animate layout-affecting properties unnecessarily +- Prefer transform-based animations when needed + +--- + +### Server & Docker considerations + +- Server routes should be stateless +- Avoid long-running synchronous operations +- Export and validation should be fast and deterministic +- Assume multiple concurrent users in production + +--- + +## Formatting & linting (do not fight the tools) + +### Prettier + +This project uses a **strict, predefined Prettier configuration**. + +Key expectations: + +- Single quotes +- Trailing commas +- 80-character line width +- Sorted imports +- Proper Svelte formatting via `prettier-plugin-svelte` + +Claude should not introduce formatting changes that contradict Prettier output. + +**IMPORTANT: Before completing any work, always run the following commands in +order and ensure they all pass. This is mandatory.** + +```sh +npm run format # auto-formats all files +npm run lint # Prettier check + ESLint + markdownlint +npm run check # svelte-check TypeScript type checking +npm run dev # verify the app starts without errors +pre-commit run --all-files # full pre-commit suite (license, YAML, markdown, ESLint, format) +``` + +The `npm run lint` command includes: + +- Prettier code formatting check +- ESLint for JavaScript/TypeScript code +- Markdown linting (markdownlint-cli2) for all .md files + +The `npm run check` command runs `svelte-kit sync && svelte-check` and must +report 0 errors before work is considered complete. Fix type errors in source +and test files; do not suppress them. + +--- + +### ESLint + +- ESLint flat config is authoritative +- TypeScript + Svelte rules are enabled +- `no-undef` is intentionally disabled +- Svelte files are type-checked via `typescript-eslint` + +**IMPORTANT: Before completing any work, always run `npm run lint` and +`npm run check` to ensure all code passes linting and type checking. This is +mandatory.** + +Do not: + +- Suppress lint rules casually +- Introduce alternative lint configs +- Disable rules without justification + +Fix code, not rules. + +--- + +## TypeScript rules + +- TypeScript is mandatory for all new code +- Prefer explicit types at module boundaries +- Avoid `any`; if used, explain why in a comment +- Shared graph types must live outside UI components + +--- + +## Export / output + +- Export formats must be stable and versioned +- No UI-only metadata in exported output +- Exporters should be pure functions +- Prefer deterministic ordering for diff-friendliness + +--- + +## Testing expectations + +- Flow validation logic must be unit tested +- Exporters must have golden-file tests +- UI tests should focus on interaction and behavior, not layout + +--- + +## Dependency management + +- Prefer latest stable versions of dependencies +- Avoid unnecessary dependencies +- Document any major dependency decisions + +--- + +## When unsure + +If requirements are ambiguous: + +- Ask before introducing new node semantics +- Prefer extending the schema over special cases +- Leave TODOs with context rather than guessing + +Clarity beats speed. +Correctness beats cleverness. + +--- + +## Creative scope & default assumptions + +Unless explicitly instructed otherwise, assume: + +- This project is a **developer-facing tool**, not a consumer product +- The goal is **clarity and correctness** over visual polish +- Default outputs should be: + - Minimal but complete + - Explicit rather than abstract + - Easy to extend later + +Claude should not: + +- Invent product features or workflows without being asked +- Add UX affordances “just in case” +- Introduce configuration surfaces prematurely +- Assume multi-user or collaborative features by default + +When asked to “build” something: + +- Start with the smallest viable, end-to-end slice +- Prefer scaffolding that can grow over finished-looking systems +- Add or extend tests to lock in behaviour +- All new user-visible UI text must use i18n (i18next), not inline strings diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 8c67a01..655438e 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -[simon@simonemms.com]. +. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6bf3836 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# Copyright 2025 - 2026 Zigflow authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM node:lts AS dev +ARG APP +ARG GIT_COMMIT +ARG VERSION +USER node +WORKDIR /home/node/app +ENV GIT_COMMIT="${GIT_COMMIT}" +ENV VERSION="${VERSION}" +COPY --chown=node:node . . +ENV HOST=0.0.0.0 +ENV PORT=5173 +EXPOSE 5173 +CMD [ "npm", "run", "dev" ] + +FROM node:lts-alpine AS builder +ARG VERSION +ENV VERSION="${VERSION}" +USER node +WORKDIR /home/node/app +COPY --chown=node:node . . +RUN npm ci \ + && npm run build \ + && npm prune --production + +FROM gcr.io/distroless/nodejs20 +ARG GIT_COMMIT +ARG GIT_REPO +ARG VERSION +WORKDIR /opt/app +ENV GIT_REPO="${GIT_REPO}" +ENV GIT_COMMIT="${GIT_COMMIT}" +ENV VERSION="${VERSION}" +ENV HOST=0.0.0.0 +ENV PORT=3000 +ENV PUBLIC_WORKFLOWS_DIR=/data +ENV NODE_ENV=production +COPY --from=builder /home/node/app/build build +COPY --from=builder /home/node/app/node_modules node_modules +COPY --from=builder /home/node/app/package.json package.json +USER 65532 +EXPOSE 3000 +VOLUME [ "/data" ] +CMD [ "build" ] diff --git a/Makefile b/Makefile deleted file mode 100644 index cf99770..0000000 --- a/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2026 Zigflow authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -cruft-update: -ifeq (,$(wildcard .cruft.json)) - @echo "Cruft not configured" -else - @cruft check || cruft update --skip-apply-ask --refresh-private-variables -endif -.PHONY: cruft-update diff --git a/README.md b/README.md index 699bc9c..31f644a 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,6 @@ -# ui +# Studio -A drag and drop UI for building Temporal workflows - - - -* [Contributing](#contributing) - * [Open in Gitpod](#open-in-gitpod) - * [Open in a container](#open-in-a-container) - * [Commit style](#commit-style) - - - - - -## Contributing - -### Open in Gitpod - -* [Open in Gitpod](https://gitpod.io/from-referrer/) +> **Work in progress.** This project is under active development and is not yet +> ready for production use. -### Open in a container - -* [Open in a container](https://code.visualstudio.com/docs/devcontainers/containers) - -### Commit style - -All commits must be done in the [Conventional Commit](https://www.conventionalcommits.org) -format. - -```git -[optional scope]: - -[optional body] - -[optional footer(s)] -``` +A drag and drop UI for building Temporal workflows diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..019b351 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,114 @@ +# Security Policy + +Zigflow is an open-source project licensed under the Apache License 2.0 and is +provided on an "AS IS" basis, without warranties or conditions of any kind. + +Security issues are taken seriously and will be addressed on a best-effort basis. + +--- + +## Supported Versions + +Security updates are provided for: + +- The `main` branch +- The most recent stable release + +Older releases may not receive security updates. Users are strongly encouraged +to upgrade to the latest version. + +There are no guaranteed response times or service level agreements. + +--- + +## Reporting a Vulnerability + +If you discover a security vulnerability, please report it privately. + +**Do not open a public GitHub issue for security vulnerabilities.** + +Instead, use one of the following: + +- GitHub Security Advisories via the repository’s Security tab +- Email: + +Please include: + +- A clear description of the vulnerability +- The affected version(s) or commit(s) +- Steps to reproduce +- A proof of concept, exploit details or logs where possible +- Any suggested mitigation + +You will receive an acknowledgement within 5 working days. +As Zigflow is maintained in spare time, response times may occasionally be longer. + +If you do not receive a response within that time, please follow up. + +--- + +## What to Expect + +After a report is received: + +1. The issue will be reviewed and severity assessed. +2. Additional information may be requested. +3. If confirmed, a fix will be developed and released. +4. The vulnerability may be disclosed publicly once a fix is available. + +Fix timelines depend on severity, complexity and maintainer availability. +There are no guaranteed remediation timelines. + +--- + +## CVEs + +Where appropriate, a CVE identifier may be requested and published for +confirmed vulnerabilities. + +If a CVE is assigned: + +- The CVE ID will be referenced in the security advisory +- Release notes will document affected versions +- Remediation guidance will be provided + +--- + +## Scope + +This policy applies to: + +- The Zigflow source code +- Official releases and distributed artefacts +- Build and packaging configuration + +Out of scope: + +- Vulnerabilities in third-party dependencies unless directly introduced by Zigflow +- Theoretical issues without a reproducible attack path +- Denial of service under unrealistic load conditions +- Social engineering or phishing attempts + +--- + +## Responsible Disclosure + +Please: + +- Avoid accessing data that does not belong to you +- Avoid modifying or deleting data +- Avoid actions that could degrade availability for other users + +If you act in good faith and follow responsible disclosure practices, no legal +action will be taken against you for reporting vulnerabilities. + +--- + +## Security Best Practices for Users + +Users of Zigflow should: + +- Run the latest supported version +- Restrict network exposure where applicable +- Follow the principle of least privilege +- Monitor logs and system behaviour for anomalies diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..cff05ce --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,59 @@ +version: '3' + +vars: + TMP_IMG: ttl.sh/zigflow-studio + TMP_IMG_TAG: 24h + +tasks: + commitlint: + desc: Run commitlint + cmds: + - npx commitlint --version + - npx commitlint --to HEAD + + cruft-update: + desc: Check/update cruft template + cmds: + - | + if [ ! -f .cruft.json ]; then + echo "Cruft not configured" + else + cruft check || cruft update --skip-apply-ask --refresh-private-variables + fi + + helm-img: + desc: Build and push temporary Helm image + cmds: + - docker build -t {{.TMP_IMG}}:{{.TMP_IMG_TAG}} . + - docker push {{.TMP_IMG}}:{{.TMP_IMG_TAG}} + + helm: + desc: Deploy Helm chart locally + cmds: + - touch values.example.yaml + - | + helm upgrade \ + --cleanup-on-fail \ + --create-namespace \ + --install \ + --namespace zigflow \ + --reset-values \ + --rollback-on-failure \ + --set image.pullPolicy=Always \ + --set image.repository={{.TMP_IMG}} \ + --set image.tag={{.TMP_IMG_TAG}} \ + --values ./values.example.yaml \ + --wait \ + zigflow ./charts/studio + + minikube: + desc: Start minikube and apply dev manifests + cmds: + - | + minikube profile list | grep minikube | grep OK || minikube start + + scan: + desc: Scan for vulnerabilities + cmds: + - trivy config --severity HIGH,CRITICAL --format table --exit-code 1 charts/studio + - trivy image --severity HIGH,CRITICAL --format table --exit-code 1 --pkg-types os,library --ignore-unfixed ghcr.io/zigflow/studio diff --git a/charts/studio/.helmignore b/charts/studio/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/studio/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/studio/Chart.yaml b/charts/studio/Chart.yaml new file mode 100644 index 0000000..c32242e --- /dev/null +++ b/charts/studio/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: studio +description: Web UI for visualising and editing Zigflow workflows +type: application +version: 0.0.0 +icon: https://raw.githubusercontent.com/zigflow/zigflow/refs/heads/main/designs/z-logo.png +home: https://zigflow.dev +maintainers: + - name: Simon Emms + email: simon@simonemms.com + url: https://simonemms.com +sources: + - https://github.com/zigflow/studio +keywords: + - DSL + - Durable Execution + - Serverless Workflow + - Temporal + - Workflow Management System + - Workflows + - YAML +annotations: + org.opencontainers.image.documentation: https://github.com/zigflow/studio/tree/main/charts/studio diff --git a/charts/studio/README.md b/charts/studio/README.md new file mode 100644 index 0000000..e36e536 --- /dev/null +++ b/charts/studio/README.md @@ -0,0 +1,56 @@ +# studio + +![Version: 0.0.0](https://img.shields.io/badge/Version-0.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) + +Web UI for visualising and editing Zigflow workflows + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| Simon Emms | | | + +## Source Code + +* + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | Node affinity | +| autoscaling.enabled | bool | `false` | Autoscaling enabled | +| autoscaling.maxReplicas | int | `100` | Maximum replicas | +| autoscaling.minReplicas | int | `1` | Minimum replicas | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | When to trigger a new replica | +| dataDir | string | `"/data"` | Path on the container where workflow YAML files are read from | +| envvars | list | `[]` | Additional environment variables | +| fullnameOverride | string | `""` | String to fully override names | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| image.repository | string | `"ghcr.io/zigflow/studio"` | Image repository | +| image.tag | string | `""` | Image tag - defaults to the chart's Version if not set | +| imagePullSecrets | list | `[]` | Docker registry secret names | +| livenessProbe.httpGet.path | string | `"/"` | Path to demonstrate app liveness | +| livenessProbe.httpGet.port | string | `"http"` | Port to demonstrate app liveness | +| nameOverride | string | `""` | String to partially override name | +| nodeSelector | object | `{}` | Node selector | +| podAnnotations | object | `{}` | Pod annotations | +| podLabels | object | `{}` | Pod labels | +| podSecurityContext | object | `{"fsGroup":1000,"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}}` | Pod's security context | +| readinessProbe.httpGet.path | string | `"/"` | Path to demonstrate app readiness | +| readinessProbe.httpGet.port | string | `"http"` | Port to demonstrate app readiness | +| replicaCount | int | `1` | Number of replicas | +| resources | object | `{}` | Configure resources available | +| securityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true,"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}}` | Container's security context | +| service.port | int | `3000` | Service port | +| service.type | string | `"ClusterIP"` | Service type | +| serviceAccount.annotations | object | `{}` | Annotations to add to the service account | +| serviceAccount.automount | bool | `true` | Automatically mount a ServiceAccount's API credentials? | +| serviceAccount.create | bool | `true` | Specifies whether a service account should be created | +| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | +| tolerations | list | `[]` | Node toleration | +| volumeMounts | list | `[]` | Additional volumeMounts on the output Deployment definition. | +| volumes | list | `[]` | Additional volumes on the output Deployment definition. | + diff --git a/charts/studio/README.tpl b/charts/studio/README.tpl new file mode 100644 index 0000000..f26dcf4 --- /dev/null +++ b/charts/studio/README.tpl @@ -0,0 +1,44 @@ +{* + Copyright 2025 - 2026 Zigflow authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*} + +# Zigflow Studio + +{{ template "chart.deprecationWarning" . }} + +[![Version](https://img.shields.io/github/v/release/zigflow/studio?label=Version&color=007ec6)](https://github.com/zigflow/studio/tree/main/charts/studio) +![Type: Application](https://img.shields.io/badge/Type-Application-informational) + +{{ template "chart.description" . }} + +{{ template "chart.homepageLine" . }} + +## TL;DR + +Be sure to set `${ZIGFLOW_VERSION}` with [your desired version](https://github.com/zigflow/studio/pkgs/container/charts%2Fstudio) + +```sh +helm install myrelease oci://ghcr.io/mrsimonemms/charts/studio@${ZIGFLOW_VERSION} +``` + +{{ template "chart.maintainersSection" . }} + +{{ template "chart.sourcesSection" . }} + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSectionHtml" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/charts/studio/templates/NOTES.txt b/charts/studio/templates/NOTES.txt new file mode 100644 index 0000000..6168617 --- /dev/null +++ b/charts/studio/templates/NOTES.txt @@ -0,0 +1,5 @@ +Deployed +======== + +App: {{ include "studio.fullname" . }} +Namespace: {{ .Release.Namespace }} diff --git a/charts/studio/templates/_helpers.tpl b/charts/studio/templates/_helpers.tpl new file mode 100644 index 0000000..8510d26 --- /dev/null +++ b/charts/studio/templates/_helpers.tpl @@ -0,0 +1,80 @@ +{* + Copyright 2025 - 2026 Zigflow authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*} + + + +{{/* +Expand the name of the chart. +*/}} +{{- define "studio.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "studio.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "studio.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "studio.labels" -}} +helm.sh/chart: {{ include "studio.chart" . }} +{{ include "studio.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "studio.selectorLabels" -}} +app.kubernetes.io/name: {{ include "studio.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "studio.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "studio.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/studio/templates/deployment.yaml b/charts/studio/templates/deployment.yaml new file mode 100644 index 0000000..2f223d1 --- /dev/null +++ b/charts/studio/templates/deployment.yaml @@ -0,0 +1,88 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "studio.fullname" . }} + labels: + {{- include "studio.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "studio.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "studio.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "studio.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.Version }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: {{ .Values.service.port | quote }} + - name: PUBLIC_WORKFLOWS_DATA_DIR + value: {{ .Values.dataDir | quote }} + {{- with .Values.envvars }} + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/studio/templates/hpa.yaml b/charts/studio/templates/hpa.yaml new file mode 100644 index 0000000..bb9f3e1 --- /dev/null +++ b/charts/studio/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "studio.fullname" . }} + labels: + {{- include "studio.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "studio.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/studio/templates/service.yaml b/charts/studio/templates/service.yaml new file mode 100644 index 0000000..87161e8 --- /dev/null +++ b/charts/studio/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "studio.fullname" . }} + labels: {{- include "studio.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: {{- include "studio.selectorLabels" . | nindent 4 }} diff --git a/charts/studio/templates/serviceaccount.yaml b/charts/studio/templates/serviceaccount.yaml new file mode 100644 index 0000000..fb4b028 --- /dev/null +++ b/charts/studio/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "studio.serviceAccountName" . }} + labels: + {{- include "studio.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/studio/templates/tests/test-connection.yaml b/charts/studio/templates/tests/test-connection.yaml new file mode 100644 index 0000000..58146b5 --- /dev/null +++ b/charts/studio/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "studio.fullname" . }}-test-connection" + labels: + {{- include "studio.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "studio.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/charts/studio/values.yaml b/charts/studio/values.yaml new file mode 100644 index 0000000..92ea980 --- /dev/null +++ b/charts/studio/values.yaml @@ -0,0 +1,110 @@ +# Default values for studio. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# -- Number of replicas +replicaCount: 1 + +# -- Path on the container where workflow YAML files are read from +dataDir: /data + +# -- Additional environment variables +envvars: [] +# - name: SOME_VAR +# value: some-value + +image: + # -- Image repository + repository: ghcr.io/zigflow/studio + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Image tag - defaults to the chart's Version if not set + tag: "" + +# -- Docker registry secret names +imagePullSecrets: [] +# -- String to partially override name +nameOverride: "" +# -- String to fully override names +fullnameOverride: "" + +serviceAccount: + # -- Specifies whether a service account should be created + create: true + # -- Automatically mount a ServiceAccount's API credentials? + automount: true + # -- Annotations to add to the service account + annotations: {} + # -- The name of the service account to use. If not set and create is true, a name is generated using the fullname template + name: "" + +# -- Pod annotations +podAnnotations: {} +# -- Pod labels +podLabels: {} + +# -- Pod's security context +podSecurityContext: + runAsNonRoot: true + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +# -- Container's security context +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + +service: + # -- Service type + type: ClusterIP + # -- Service port + port: 3000 + +# -- Configure resources available +resources: {} + +livenessProbe: + httpGet: + # -- Path to demonstrate app liveness + path: / + # -- Port to demonstrate app liveness + port: http +readinessProbe: + httpGet: + # -- Path to demonstrate app readiness + path: / + # -- Port to demonstrate app readiness + port: http + +autoscaling: + # -- Autoscaling enabled + enabled: false + # -- Minimum replicas + minReplicas: 1 + # -- Maximum replicas + maxReplicas: 100 + # -- When to trigger a new replica + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# -- Additional volumes on the output Deployment definition. +volumes: [] + +# -- Additional volumeMounts on the output Deployment definition. +volumeMounts: [] + +# -- Node selector +nodeSelector: {} + +# -- Node toleration +tolerations: [] + +# -- Node affinity +affinity: {} diff --git a/eslint.config.js b/eslint.config.js index 5ce6254..8c3af12 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,5 @@ /* - * Copyright 2026 Zigflow authors + * Copyright 2025 - 2026 Zigflow authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,37 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { includeIgnoreFile } from '@eslint/compat'; import js from '@eslint/js'; import prettier from 'eslint-config-prettier'; import svelte from 'eslint-plugin-svelte'; import globals from 'globals'; +import { fileURLToPath } from 'node:url'; import ts from 'typescript-eslint'; -export default ts.config( - js.configs.recommended, - ...ts.configs.recommended, - ...svelte.configs['flat/recommended'], - prettier, - ...svelte.configs['flat/prettier'], - { - languageOptions: { - globals: { - ...globals.browser, - ...globals.node, - }, - }, - }, - { - files: ['**/*.svelte'], +import svelteConfig from './svelte.config.js'; - languageOptions: { - parserOptions: { - parser: ts.parser, - }, - }, - }, - { - ignores: ['build/', '.svelte-kit/', 'dist/'], - }, +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs['flat/recommended'], + prettier, + ...svelte.configs['flat/prettier'], + { + languageOptions: { + globals: { ...globals.browser, ...globals.node }, + }, + rules: { + // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. + // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors + 'no-undef': 'off', + // eslint-plugin-svelte@3.15.x crashes with TypeError when visiting boolean + // shorthand attributes (e.g. `fitView` with no value). Disable until fixed upstream. + // see: https://github.com/sveltejs/eslint-plugin-svelte/issues + 'svelte/no-navigation-without-resolve': 'off', + }, + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig, + }, + }, + }, ); diff --git a/package-lock.json b/package-lock.json index 151771b..7430bef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,37 +1,161 @@ { - "name": "ui", + "name": "@zigflow/studio", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ui", + "name": "@zigflow/studio", "version": "0.0.0", "license": "Apache-2.0", "devDependencies": { - "@sveltejs/adapter-static": "^3.0.1", - "@sveltejs/kit": "^2.5.10", - "@sveltejs/vite-plugin-svelte": "^4.0.0", - "@types/eslint": "^9.6.0", - "eslint": "^9.7.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.39.0", - "globals": "^15.0.0", - "prettier": "^3.2.5", - "prettier-plugin-organize-imports": "^3.2.4", - "prettier-plugin-svelte": "^3.2.3", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "^5.4.5", - "typescript-eslint": "^8.0.0", - "vite": "^5.2.11", - "vitest": "^2.0.0" + "@eslint/compat": "^2.0.3", + "@eslint/js": "^9.39.4", + "@playwright/test": "^1.58.2", + "@sveltejs/adapter-node": "^5.5.4", + "@sveltejs/kit": "^2.53.4", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@trivago/prettier-plugin-sort-imports": "^6.0.2", + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.3.5", + "@xyflow/svelte": "^1.5.1", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.15.1", + "globals": "^17.4.0", + "i18next": "^25.8.18", + "js-yaml": "^4.1.1", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.5.1", + "svelte": "^5.53.8", + "svelte-check": "^4.4.5", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -42,13 +166,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -59,13 +183,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -76,13 +200,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -93,13 +217,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -110,13 +234,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -127,13 +251,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -144,13 +268,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -161,13 +285,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -178,13 +302,11 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.27.3", "cpu": [ "arm64" ], @@ -195,13 +317,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -212,13 +334,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -229,13 +351,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -246,13 +368,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -263,13 +385,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -280,13 +402,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -297,13 +419,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -314,13 +436,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -331,13 +470,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -348,13 +504,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -365,13 +538,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -382,13 +555,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -399,13 +572,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -416,13 +589,11 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -440,8 +611,6 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -453,18 +622,33 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/compat": { + "version": "2.0.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "eslint": "^8.40 || 9 || 10" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, "node_modules/@eslint/config-array": { "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -476,10 +660,28 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -489,10 +691,8 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/core": { + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -502,10 +702,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/eslintrc": { "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { @@ -526,10 +735,17 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -539,10 +755,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -554,8 +779,6 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -564,8 +787,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -576,10 +797,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -588,8 +818,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -602,8 +830,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -616,8 +842,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -630,8 +854,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -641,8 +863,6 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -652,8 +872,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -662,15 +880,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -678,13 +892,113 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, "license": "MIT" }, + "node_modules/@rollup/plugin-commonjs": { + "version": "29.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -799,8 +1113,6 @@ }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -1037,35 +1349,41 @@ }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, + "node_modules/@svelte-put/shortcut": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "svelte": "^5.1.0" + } + }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", - "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" } }, - "node_modules/@sveltejs/adapter-static": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", - "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "node_modules/@sveltejs/adapter-node": { + "version": "5.5.4", "dev": true, "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.59.0" + }, "peerDependencies": { - "@sveltejs/kit": "^2.0.0" + "@sveltejs/kit": "^2.4.0" } }, "node_modules/@sveltejs/kit": { "version": "2.53.4", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz", - "integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==", "dev": true, "license": "MIT", "dependencies": { @@ -1105,88 +1423,162 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", - "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", + "version": "6.2.4", "dev": true, "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", - "debug": "^4.3.7", + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.12", - "vitefu": "^1.0.3" + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "svelte": "^5.0.0-next.96 || ^5.0.0", - "vite": "^5.0.0" + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", - "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", + "version": "5.0.2", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.7" + "obug": "^2.1.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "6.0.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "javascript-natural-sort": "^0.7.1", + "lodash-es": "^4.17.21", + "minimatch": "^9.0.0", + "parse-imports-exports": "^0.2.4" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": ">= 20" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", - "svelte": "^5.0.0-next.96 || ^5.0.0", - "vite": "^5.0.0" + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-ember-template-tag": ">= 2.0.0", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-ember-template-tag": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } } }, "node_modules/@types/cookie": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "node_modules/@types/d3-color": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" } }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", "dev": true, "license": "MIT" }, "node_modules/@types/trusted-types": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1214,8 +1606,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -1224,8 +1614,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", "dependencies": { @@ -1249,8 +1637,6 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", "dev": true, "license": "MIT", "dependencies": { @@ -1271,8 +1657,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", "dev": true, "license": "MIT", "dependencies": { @@ -1289,8 +1673,6 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", "dev": true, "license": "MIT", "engines": { @@ -1306,8 +1688,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1331,8 +1711,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", "dev": true, "license": "MIT", "engines": { @@ -1345,8 +1723,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1373,8 +1749,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -1383,8 +1757,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -1396,8 +1768,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -1412,8 +1782,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1436,8 +1804,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", "dev": true, "license": "MIT", "dependencies": { @@ -1453,135 +1819,46 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "5.0.1", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "node_modules/@xyflow/svelte": { + "version": "1.5.1", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "@svelte-put/shortcut": "^4.1.0", + "@xyflow/system": "0.0.75" }, - "funding": { - "url": "https://opencollective.com/vitest" + "peerDependencies": { + "svelte": "^5.25.0" } }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "node_modules/@xyflow/system": { + "version": "0.0.75", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" } }, "node_modules/acorn": { "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -1593,8 +1870,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1603,8 +1878,6 @@ }, "node_modules/ajv": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1620,8 +1893,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -1636,35 +1907,19 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/axobject-query": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1673,63 +1928,27 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "2.0.2", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "balanced-match": "^1.0.0" } }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -1743,20 +1962,8 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { @@ -1771,8 +1978,6 @@ }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "dev": true, "license": "MIT", "engines": { @@ -1781,8 +1986,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1794,22 +1997,21 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", "dev": true, "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/cookie": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "license": "MIT", "engines": { @@ -1818,8 +2020,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -1833,8 +2033,6 @@ }, "node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", "bin": { @@ -1844,10 +2042,104 @@ "node": ">=4" } }, + "node_modules/d3-color": { + "version": "3.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1862,27 +2154,13 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", "engines": { @@ -1891,22 +2169,11 @@ }, "node_modules/devalue": { "version": "5.6.3", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", - "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.27.3", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1914,38 +2181,39 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -1957,8 +2225,6 @@ }, "node_modules/eslint": { "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2015,62 +2281,44 @@ } } }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, "node_modules/eslint-config-prettier": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", - "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "version": "10.1.8", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } }, "node_modules/eslint-plugin-svelte": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", - "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "version": "3.15.1", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@jridgewell/sourcemap-codec": "^1.4.15", - "eslint-compat-utils": "^0.5.1", + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", - "known-css-properties": "^0.35.0", - "postcss": "^8.4.38", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.1.0", - "semver": "^7.6.2", - "svelte-eslint-parser": "^0.43.0" + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.4.0" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { @@ -2079,10 +2327,19 @@ } } }, + "node_modules/eslint-plugin-svelte/node_modules/globals": { + "version": "16.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2098,8 +2355,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2109,17 +2364,44 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/esm-env": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "dev": true, "license": "MIT" }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2136,8 +2418,6 @@ }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2149,8 +2429,6 @@ }, "node_modules/esrap": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", - "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2159,8 +2437,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2172,8 +2448,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2181,60 +2455,35 @@ } }, "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "version": "2.0.2", "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } + "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -2251,8 +2500,6 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2264,8 +2511,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -2281,8 +2526,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -2295,15 +2538,13 @@ }, "node_modules/flatted": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2315,10 +2556,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -2329,9 +2576,7 @@ } }, "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "version": "17.4.0", "dev": true, "license": "MIT", "engines": { @@ -2343,18 +2588,57 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/i18next": { + "version": "25.8.18", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.18.tgz", + "integrity": "sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -2363,8 +2647,6 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2380,18 +2662,28 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -2400,8 +2692,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -2411,27 +2701,36 @@ "node": ">=0.10.0" } }, + "node_modules/is-module": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "version": "1.2.1", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.6" + "@types/estree": "*" } }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2441,31 +2740,34 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -2474,8 +2776,6 @@ }, "node_modules/kleur": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "dev": true, "license": "MIT", "engines": { @@ -2483,16 +2783,12 @@ } }, "node_modules/known-css-properties": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", - "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "version": "0.37.0", "dev": true, "license": "MIT" }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2505,8 +2801,6 @@ }, "node_modules/lilconfig": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, "license": "MIT", "engines": { @@ -2515,15 +2809,11 @@ }, "node_modules/locate-character": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "dev": true, "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -2536,24 +2826,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/lodash-es": { + "version": "4.17.23", "dev": true, "license": "MIT" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "node_modules/lodash.merge": { + "version": "4.6.2", "dev": true, "license": "MIT" }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2561,22 +2845,21 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "9.0.9", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mri": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true, "license": "MIT", "engines": { @@ -2585,8 +2868,6 @@ }, "node_modules/mrmime": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", "engines": { @@ -2595,15 +2876,11 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -2621,15 +2898,20 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -2646,8 +2928,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2662,8 +2942,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -2678,8 +2956,6 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -2689,10 +2965,21 @@ "node": ">=6" } }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -2701,42 +2988,24 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "node_modules/path-parse": { + "version": "1.0.7", "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -2746,10 +3015,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.58.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -2777,8 +3072,6 @@ }, "node_modules/postcss-load-config": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", "dev": true, "license": "MIT", "dependencies": { @@ -2805,27 +3098,41 @@ } } }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "version": "7.0.1", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">=18.0" }, "peerDependencies": { - "postcss": "^8.3.3" + "postcss": "^8.4.31" } }, "node_modules/postcss-scss": { "version": "4.0.9", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", - "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", "dev": true, "funding": [ { @@ -2850,9 +3157,7 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.1.1", "dev": true, "license": "MIT", "dependencies": { @@ -2865,8 +3170,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -2875,8 +3178,6 @@ }, "node_modules/prettier": { "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "bin": { @@ -2889,31 +3190,8 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-plugin-organize-imports": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", - "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@volar/vue-language-plugin-pug": "^1.0.4", - "@volar/vue-typescript": "^1.0.4", - "prettier": ">=2.0", - "typescript": ">=2.9" - }, - "peerDependenciesMeta": { - "@volar/vue-language-plugin-pug": { - "optional": true - }, - "@volar/vue-typescript": { - "optional": true - } - } - }, "node_modules/prettier-plugin-svelte": { "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", - "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2923,8 +3201,6 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -2933,8 +3209,6 @@ }, "node_modules/readdirp": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { @@ -2945,10 +3219,27 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/resolve": { + "version": "1.22.11", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -2957,8 +3248,6 @@ }, "node_modules/rollup": { "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -3002,8 +3291,6 @@ }, "node_modules/sade": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "dev": true, "license": "MIT", "dependencies": { @@ -3015,8 +3302,6 @@ }, "node_modules/semver": { "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3028,15 +3313,11 @@ }, "node_modules/set-cookie-parser": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", - "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==", "dev": true, "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -3048,25 +3329,14 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, "node_modules/sirv": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -3080,32 +3350,14 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -3117,8 +3369,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3128,10 +3378,19 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/svelte": { "version": "5.53.8", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.8.tgz", - "integrity": "sha512-UD++BnEc3PUFgjin381LiMHzDjT187Fy+KsPZxvaKrYPZqR0GQ/Ha8h7GDoegIF8tFl1uogoNUejKgcRk77T2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3158,8 +3417,6 @@ }, "node_modules/svelte-check": { "version": "4.4.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", - "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", "dev": true, "license": "MIT", "dependencies": { @@ -3181,20 +3438,21 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", - "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "version": "1.6.0", "dev": true, "license": "MIT", "dependencies": { - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "postcss": "^8.4.39", - "postcss-scss": "^4.0.9" + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0", + "semver": "^7.7.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0", + "pnpm": "10.30.3" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -3208,72 +3466,16 @@ } } }, - "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "@types/estree": "^1.0.6" } }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3287,40 +3489,8 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/totalist": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", "engines": { @@ -3329,8 +3499,6 @@ }, "node_modules/ts-api-utils": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -3342,8 +3510,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -3355,8 +3521,6 @@ }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3369,8 +3533,6 @@ }, "node_modules/typescript-eslint": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", - "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", "dev": true, "license": "MIT", "dependencies": { @@ -3391,10 +3553,13 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3403,27 +3568,26 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "license": "MIT" }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "7.3.1", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3432,19 +3596,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -3465,36 +3635,32 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/vitefu": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", - "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", "dev": true, "license": "MIT", "workspaces": [ @@ -3511,76 +3677,8 @@ } } }, - "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -3593,47 +3691,16 @@ "node": ">= 8" } }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -3645,8 +3712,6 @@ }, "node_modules/zimmerframe": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", - "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "dev": true, "license": "MIT" } diff --git a/package.json b/package.json index 63399f2..695c9cc 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,48 @@ { - "name": "ui", - "version": "0.0.0", + "name": "@zigflow/studio", "description": "A drag and drop UI for building Temporal workflows", - "author": "Zigflow authors ", "private": true, "license": "Apache-2.0", + "version": "0.0.0", + "type": "module", "scripts": { "build": "vite build", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "dev": "vite dev", - "format": "prettier --plugin prettier-plugin-svelte --plugin prettier-plugin-organize-imports --write .", - "lint": "prettier --plugin prettier-plugin-svelte --plugin prettier-plugin-organize-imports --check . && eslint .", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", "preview": "vite preview", - "test": "npm run test:unit", - "test:unit": "vitest" + "prepare": "svelte-kit sync || echo ''", + "start": "node build", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed", + "test:e2e:debug": "playwright test --debug" }, "devDependencies": { - "@sveltejs/adapter-static": "^3.0.1", - "@sveltejs/kit": "^2.5.10", - "@sveltejs/vite-plugin-svelte": "^4.0.0", - "@types/eslint": "^9.6.0", - "eslint": "^9.7.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.39.0", - "globals": "^15.0.0", - "prettier": "^3.2.5", - "prettier-plugin-organize-imports": "^3.2.4", - "prettier-plugin-svelte": "^3.2.3", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "^5.4.5", - "typescript-eslint": "^8.0.0", - "vite": "^5.2.11", - "vitest": "^2.0.0" - }, - "type": "module" + "@eslint/compat": "^2.0.3", + "@eslint/js": "^9.39.4", + "@playwright/test": "^1.58.2", + "@sveltejs/adapter-node": "^5.5.4", + "@sveltejs/kit": "^2.53.4", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@trivago/prettier-plugin-sort-imports": "^6.0.2", + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.3.5", + "@xyflow/svelte": "^1.5.1", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.15.1", + "globals": "^17.4.0", + "i18next": "^25.8.18", + "js-yaml": "^4.1.1", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.5.1", + "svelte": "^5.53.8", + "svelte-check": "^4.4.5", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^7.3.1" + } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..9873c89 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + // Start with Chromium only to reduce noise while stabilising + // Re-enable others later if needed + ], + + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: true, + timeout: 120_000, + }, +}); diff --git a/skaffold.yaml b/skaffold.yaml new file mode 100644 index 0000000..2d2df9e --- /dev/null +++ b/skaffold.yaml @@ -0,0 +1,48 @@ +apiVersion: skaffold/v4beta13 +kind: Config +metadata: + name: studio + +build: + artifacts: + - image: studio + docker: + dockerfile: Dockerfile + target: dev + sync: + manual: + - src: "src/**/*" + dest: /home/node/app + - src: "static/**/*" + dest: /home/node/app + local: + push: false + useBuildkit: true + +deploy: + helm: + releases: + - name: studio + chartPath: charts/studio + namespace: zigflow + createNamespace: true + setValueTemplates: + image.repository: "{{.IMAGE_REPO_studio}}" + image.tag: "{{.IMAGE_TAG_studio}}" + setValues: + image: + pullPolicy: Never + securityContext: + readOnlyRootFilesystem: false + +portForward: + - resourceType: Service + resourceName: studio + namespace: zigflow + port: 3000 + localPort: 3000 + +profiles: + - name: minikube + activation: + - kubeContext: minikube diff --git a/src/app.d.ts b/src/app.d.ts index 69ea6a6..e8ed002 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,5 +1,5 @@ /* - * Copyright 2026 Zigflow authors + * Copyright 2025 - 2026 Zigflow authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,13 @@ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } } export {}; diff --git a/src/app.html b/src/app.html index 3f1bc28..f95c4be 100644 --- a/src/app.html +++ b/src/app.html @@ -1,5 +1,5 @@ + + + + + + diff --git a/src/lib/ui/Canvas.svelte b/src/lib/ui/Canvas.svelte new file mode 100644 index 0000000..ad4c7ff --- /dev/null +++ b/src/lib/ui/Canvas.svelte @@ -0,0 +1,391 @@ + + + + +
+ + + + +
+ + diff --git a/src/lib/ui/ContextIndicator.svelte b/src/lib/ui/ContextIndicator.svelte new file mode 100644 index 0000000..e91e309 --- /dev/null +++ b/src/lib/ui/ContextIndicator.svelte @@ -0,0 +1,53 @@ + + + + + + +{#if label} +
+ {label} +
+{/if} + + diff --git a/src/lib/ui/FlowNode.svelte b/src/lib/ui/FlowNode.svelte new file mode 100644 index 0000000..c0571cc --- /dev/null +++ b/src/lib/ui/FlowNode.svelte @@ -0,0 +1,408 @@ + + + + + + +
cb?.onselect(id)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') cb?.onselect(id); + }} + role="button" + tabindex="0" +> + + + {#if isStructural} + +
+ + + + {data.typeLabel} + {data.label} +
+ + {#if data.loopExpression !== undefined} +
+ + {data.loopExpression} +
+ {/if} + + {#if data.navRows && data.navRows.length > 0} +
    + {#each data.navRows as row (row.id)} +
  • + +
  • + {/each} +
+ {/if} + {:else} + +
+ + + +
+ {data.typeLabel} + {data.label} +
+
+ {/if} + + +
+ + diff --git a/src/lib/ui/Inspector.svelte b/src/lib/ui/Inspector.svelte new file mode 100644 index 0000000..1492b7b --- /dev/null +++ b/src/lib/ui/Inspector.svelte @@ -0,0 +1,1072 @@ + + + + + { + if (e.key === 'Escape' && confirmingDelete) confirmingDelete = false; + }} +/> + + + +{#if confirmingDelete} + +{/if} + + diff --git a/src/lib/ui/Sidebar.svelte b/src/lib/ui/Sidebar.svelte new file mode 100644 index 0000000..7148b02 --- /dev/null +++ b/src/lib/ui/Sidebar.svelte @@ -0,0 +1,656 @@ + + + + + { + if (e.key === 'Escape' && confirmDeleteId) handleCancelDelete(); + }} +/> + + + + +{#if confirmDeleteId} + +{/if} + + diff --git a/src/lib/ui/canvas-context.ts b/src/lib/ui/canvas-context.ts new file mode 100644 index 0000000..f52e7cd --- /dev/null +++ b/src/lib/ui/canvas-context.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Shared context key and type for Canvas → FlowNode callback communication. +// Keeping callbacks out of node data ensures structuredClone can succeed on +// node objects, which silences SvelteFlow's "Use $state.raw for nodes" warning. + +export const CANVAS_CALLBACKS_KEY = 'canvas-callbacks'; + +export type CanvasCallbacks = { + onselect: (nodeId: string) => void; + onenternode: (nodeId: string) => void; + onenterbranch: (nodeId: string, branchId: string) => void; +}; diff --git a/src/lib/ui/inspector/CommonFields.svelte b/src/lib/ui/inspector/CommonFields.svelte new file mode 100644 index 0000000..fe3b6cc --- /dev/null +++ b/src/lib/ui/inspector/CommonFields.svelte @@ -0,0 +1,488 @@ + + + + +
+

{t('inspector.common.title')}

+ + +
+ + handleIfChange(e.currentTarget.value)} + /> +
+ + +

{t('inspector.common.metadata.title')}

+ + {#if metaEntries.length > 0} +
    + {#each metaEntries as [key, val], i (i)} + {@const override = getOverride(key, val)} + {@const vError = valueErrors[key] ?? null} + {@const kError = keyErrors[key] ?? null} +
  • + +
    + handleKeyChange(i, key, e.currentTarget.value)} + /> + +
    + {#if kError !== null} + + {/if} + +
    + + handleValueChange(i, key, val, e.currentTarget.value)} + /> + +
    + {#if vError !== null} + + {/if} +
  • + {/each} +
+ {/if} + + +
+ + diff --git a/src/lib/ui/node-editors/ArgumentsList.svelte b/src/lib/ui/node-editors/ArgumentsList.svelte new file mode 100644 index 0000000..a3387a7 --- /dev/null +++ b/src/lib/ui/node-editors/ArgumentsList.svelte @@ -0,0 +1,473 @@ + + + + + + + + + +{#if args.length > 0} +
    + {#each args as arg, i (i)} +
  • + {#if isActivityComplexArg(arg)} + +
    + + {t('inspector.callActivity.arguments.complexHint')} + +
    + + + +
    +
    + {:else} + {@const override = getOverride(i, arg)} + {@const error = argErrors[i] ?? null} +
    + handleValueChange(i, e.currentTarget.value)} + /> + +
    + + + +
    +
    + {#if error !== null} + + {/if} + {/if} +
  • + {/each} +
+{/if} + + + + diff --git a/src/lib/ui/node-editors/CallActivityEditor.svelte b/src/lib/ui/node-editors/CallActivityEditor.svelte new file mode 100644 index 0000000..3f29c15 --- /dev/null +++ b/src/lib/ui/node-editors/CallActivityEditor.svelte @@ -0,0 +1,182 @@ + + + + +
+

{t('inspector.callActivity.title')}

+ + +
+ + handleNameChange(e.currentTarget.value)} + /> + {#if config.name === ''} +

+ {t('inspector.callActivity.activityNameRequired')} +

+ {/if} +
+ + +
+ + handleTaskQueueChange(e.currentTarget.value)} + /> + {#if !config.taskQueue} +

+ {t('inspector.callActivity.taskQueueRequired')} +

+ {/if} +
+ + +
+

+ {t('inspector.callActivity.arguments.title')} +

+ +
+
+ + diff --git a/src/lib/ui/node-editors/CallGrpcEditor.svelte b/src/lib/ui/node-editors/CallGrpcEditor.svelte new file mode 100644 index 0000000..011d317 --- /dev/null +++ b/src/lib/ui/node-editors/CallGrpcEditor.svelte @@ -0,0 +1,431 @@ + + + + +
+

{t('inspector.callGrpc.title')}

+ + +
+

{t('inspector.callGrpc.proto.title')}

+
+ + handleProtoEndpointChange(e.currentTarget.value)} + /> +
+
+ + +
+

{t('inspector.callGrpc.service.title')}

+
+ + handleServiceNameChange(e.currentTarget.value)} + /> +
+
+ + handleHostChange(e.currentTarget.value)} + /> +
+
+ + handlePortChange(e.currentTarget.value)} + /> + {#if portError} + + {/if} +
+
+ + +
+
+ + handleMethodChange(e.currentTarget.value)} + /> +
+
+ + +
+

{t('inspector.callGrpc.arguments.title')}

+ + {#if argEntries.length > 0} +
    + {#each argEntries as [key, val], i (i)} +
  • +
    + + handleArgKeyChange(i, key, e.currentTarget.value)} + /> + + handleArgValueChange(i, key, e.currentTarget.value)} + /> + +
    +
  • + {/each} +
+ {/if} + + +
+
+ + diff --git a/src/lib/ui/node-editors/CallHttpEditor.svelte b/src/lib/ui/node-editors/CallHttpEditor.svelte new file mode 100644 index 0000000..ff4679f --- /dev/null +++ b/src/lib/ui/node-editors/CallHttpEditor.svelte @@ -0,0 +1,441 @@ + + + + +
+

{t('inspector.callHttp.title')}

+ + +
+ + +
+ + +
+ + handleEndpointChange(e.currentTarget.value)} + /> + {#if endpointError} + + {/if} +
+ + +
+

{t('inspector.callHttp.headers.title')}

+ + {#if headerEntries.length > 0} +
    + {#each headerEntries as [key, val], i (i)} +
  • +
    + + handleHeaderKeyChange(i, key, e.currentTarget.value)} + /> + + handleHeaderValueChange(i, key, e.currentTarget.value)} + /> + +
    +
  • + {/each} +
+ {/if} + + +
+ + +
+

{t('inspector.callHttp.body.title')}

+ + {#if config.body !== undefined} + + + {:else} + + {/if} +
+
+ + diff --git a/src/lib/ui/node-editors/ForkNodeEditor.svelte b/src/lib/ui/node-editors/ForkNodeEditor.svelte new file mode 100644 index 0000000..6393bfd --- /dev/null +++ b/src/lib/ui/node-editors/ForkNodeEditor.svelte @@ -0,0 +1,78 @@ + + + + +
+ +

{t('inspector.fork.competeHint')}

+
+ + diff --git a/src/lib/ui/node-editors/ListenTaskEditor.svelte b/src/lib/ui/node-editors/ListenTaskEditor.svelte new file mode 100644 index 0000000..e309ca9 --- /dev/null +++ b/src/lib/ui/node-editors/ListenTaskEditor.svelte @@ -0,0 +1,488 @@ + + + + +
+

{t('inspector.listen.title')}

+ + +
+ + +
+ + + {#each config.events as event, i (i)} +
+ {#if isMultiEventMode} +
+ + {t('inspector.listen.events.event', { index: i + 1 })} + + +
+ {/if} + + +
+ + handleIdChange(i, e.currentTarget.value)} + /> + {#if !event.id.trim()} +

{t('inspector.listen.id.required')}

+ {/if} +
+ + +
+ + +
+ + +
+ + handleAcceptIfChange(i, e.currentTarget.value)} + /> +
+ + +
+ + handleDataContentTypeChange(i, e.currentTarget.value)} + /> +
+ + +
+

{t('inspector.listen.data.title')}

+ {#if event.data && Object.keys(event.data).length > 0} + + + + + + + + + + {#each Object.entries(event.data) as [key, val], rowIdx (rowIdx)} + + + + + + {/each} + +
{t('inspector.listen.data.keyLabel')}{t('inspector.listen.data.valueLabel')}
+ + handleDataKeyChange(i, key, e.currentTarget.value)} + /> + + + handleDataValueChange(i, key, e.currentTarget.value)} + /> + + +
+ {/if} + +
+
+ {/each} + + {#if isMultiEventMode} + + {/if} +
+ + diff --git a/src/lib/ui/node-editors/LoopNodeEditor.svelte b/src/lib/ui/node-editors/LoopNodeEditor.svelte new file mode 100644 index 0000000..7b84f94 --- /dev/null +++ b/src/lib/ui/node-editors/LoopNodeEditor.svelte @@ -0,0 +1,192 @@ + + + + +
+

{t('inspector.loop.configuration')}

+ +
+
{t('inspector.loop.collection')}
+
+ onupdate({ ...loopNode, in: e.currentTarget.value })} + /> + {#if collectionEmpty} +

+ {t('validation.loop.collectionRequired')} +

+ {/if} +
+ +
{t('inspector.loop.itemVariable')}
+
+ handleOptional('each', e.currentTarget.value)} + /> + {#if itemInvalid} +

+ {t('validation.loop.invalidIdentifier')} +

+ {/if} +
+ +
{t('inspector.loop.indexVariable')}
+
+ handleOptional('at', e.currentTarget.value)} + /> + {#if indexInvalid} +

+ {t('validation.loop.invalidIdentifier')} +

+ {/if} +
+ +
{t('inspector.loop.breakCondition')}
+
+ handleOptional('while', e.currentTarget.value)} + /> +
+
+
+ + diff --git a/src/lib/ui/node-editors/RaiseTaskEditor.svelte b/src/lib/ui/node-editors/RaiseTaskEditor.svelte new file mode 100644 index 0000000..6d95f2d --- /dev/null +++ b/src/lib/ui/node-editors/RaiseTaskEditor.svelte @@ -0,0 +1,361 @@ + + + + +
+

{t('inspector.raise.title')}

+ + {#if !definition} +

{t('inspector.raise.noDefinition')}

+ {/if} + + +
+ + +
+ + +
+ + handleTitleChange(e.currentTarget.value)} + /> +
+ + +
+ + +
+ + +
+ + +
+ + {#if definition} + + {/if} +
+ + diff --git a/src/lib/ui/node-editors/RunContainerEditor.svelte b/src/lib/ui/node-editors/RunContainerEditor.svelte new file mode 100644 index 0000000..631643f --- /dev/null +++ b/src/lib/ui/node-editors/RunContainerEditor.svelte @@ -0,0 +1,631 @@ + + + + +
+

{t('inspector.run.container.title')}

+ + + {#if config.ports !== undefined} + + {/if} + + +
+ + handleImageChange(e.currentTarget.value)} + /> + {#if imageError} + + {/if} +
+ + +
+ + handleWorkingDirChange(e.currentTarget.value)} + /> +
+ + +
+ + +
+ + +
+ + handleAwaitChange(e.currentTarget.checked)} + /> +
+ + +
+

+ {t('inspector.run.container.arguments.title')} +

+ + emit({ arguments: next.length === 0 ? undefined : next })} + /> +
+ + +
+

{t('inspector.run.container.env.title')}

+ + {#if envEntries.length > 0} +
    + {#each envEntries as [key, val], i (i)} + {@const override = getEnvOverride(key, val)} + {@const error = envErrors[key] ?? null} +
  • +
    + + handleEnvKeyChange(i, key, e.currentTarget.value)} + /> + +
    +
    + + handleEnvValueChange(i, key, val, e.currentTarget.value)} + /> + +
    + {#if error !== null} + + {/if} +
  • + {/each} +
+ {/if} + + +
+
+ + diff --git a/src/lib/ui/node-editors/RunScriptEditor.svelte b/src/lib/ui/node-editors/RunScriptEditor.svelte new file mode 100644 index 0000000..6703f0d --- /dev/null +++ b/src/lib/ui/node-editors/RunScriptEditor.svelte @@ -0,0 +1,520 @@ + + + + +
+

{t('inspector.run.script.title')}

+ + +
+ + +
+ + +
+ + + {#if codeError} + + {/if} +
+ + +
+

+ {t('inspector.run.script.arguments.title')} +

+ + emit({ arguments: next.length === 0 ? undefined : next })} + /> +
+ + +
+

{t('inspector.run.script.env.title')}

+ + {#if envEntries.length > 0} +
    + {#each envEntries as [key, val], i (i)} + {@const override = getEnvOverride(key, val)} + {@const error = envErrors[key] ?? null} +
  • +
    + + handleEnvKeyChange(i, key, e.currentTarget.value)} + /> + +
    +
    + + handleEnvValueChange(i, key, val, e.currentTarget.value)} + /> + +
    + {#if error !== null} + + {/if} +
  • + {/each} +
+ {/if} + + +
+
+ + diff --git a/src/lib/ui/node-editors/RunShellEditor.svelte b/src/lib/ui/node-editors/RunShellEditor.svelte new file mode 100644 index 0000000..c4a0991 --- /dev/null +++ b/src/lib/ui/node-editors/RunShellEditor.svelte @@ -0,0 +1,524 @@ + + + + +
+

{t('inspector.run.shell.title')}

+ + +
+ + handleCommandChange(e.currentTarget.value)} + /> + {#if commandError} + + {/if} +
+ + +
+ + handleWorkingDirChange(e.currentTarget.value)} + /> +
+ + +
+ + handleAwaitChange(e.currentTarget.checked)} + /> +
+ + +
+

+ {t('inspector.run.shell.arguments.title')} +

+ + emit({ arguments: next.length === 0 ? undefined : next })} + /> +
+ + +
+

{t('inspector.run.shell.env.title')}

+ + {#if envEntries.length > 0} +
    + {#each envEntries as [key, val], i (i)} + {@const override = getEnvOverride(key, val)} + {@const error = envErrors[key] ?? null} +
  • +
    + + handleEnvKeyChange(i, key, e.currentTarget.value)} + /> + +
    +
    + + handleEnvValueChange(i, key, val, e.currentTarget.value)} + /> + +
    + {#if error !== null} + + {/if} +
  • + {/each} +
+ {/if} + + +
+
+ + diff --git a/src/lib/ui/node-editors/RunWorkflowEditor.svelte b/src/lib/ui/node-editors/RunWorkflowEditor.svelte new file mode 100644 index 0000000..c1af2bd --- /dev/null +++ b/src/lib/ui/node-editors/RunWorkflowEditor.svelte @@ -0,0 +1,200 @@ + + + + +
+

{t('inspector.run.workflow.title')}

+ + +
+ + handleNameChange(e.currentTarget.value)} + /> + {#if nameError} + + {/if} +
+ + +
+ + handleNamespaceChange(e.currentTarget.value)} + /> +
+ + +
+ + handleAwaitChange(e.currentTarget.checked)} + /> +
+ {#if config.await === false} +

{t('inspector.run.await.help.workflow')}

+ {/if} +
+ + diff --git a/src/lib/ui/node-editors/SetNodeEditor.svelte b/src/lib/ui/node-editors/SetNodeEditor.svelte new file mode 100644 index 0000000..7732d9f --- /dev/null +++ b/src/lib/ui/node-editors/SetNodeEditor.svelte @@ -0,0 +1,383 @@ + + + + +
+

{t('inspector.set.title')}

+ + {#if entries.length > 0} +
    + {#each entries as [key, val], i (i)} + {@const override = getOverride(key, val)} + {@const error = errors[key] ?? null} +
  • + +
    + handleKeyChange(i, key, e.currentTarget.value)} + /> + +
    + +
    + + handleValueChange(i, key, val, e.currentTarget.value)} + /> + +
    + + {#if error !== null} + + {/if} +
  • + {/each} +
+ {/if} + + +
+ + diff --git a/src/lib/ui/node-editors/SwitchNodeEditor.svelte b/src/lib/ui/node-editors/SwitchNodeEditor.svelte new file mode 100644 index 0000000..994150f --- /dev/null +++ b/src/lib/ui/node-editors/SwitchNodeEditor.svelte @@ -0,0 +1,30 @@ + + + diff --git a/src/lib/ui/node-editors/TryNodeEditor.svelte b/src/lib/ui/node-editors/TryNodeEditor.svelte new file mode 100644 index 0000000..f2fca8a --- /dev/null +++ b/src/lib/ui/node-editors/TryNodeEditor.svelte @@ -0,0 +1,30 @@ + + + diff --git a/src/lib/ui/node-editors/WaitNodeEditor.svelte b/src/lib/ui/node-editors/WaitNodeEditor.svelte new file mode 100644 index 0000000..24a9f13 --- /dev/null +++ b/src/lib/ui/node-editors/WaitNodeEditor.svelte @@ -0,0 +1,135 @@ + + + + +
+

{t('inspector.wait.title')}

+ +
+ {#each fields as { key, label } (key)} +
{label}
+
+ handleField(key, e.currentTarget.value)} + /> +
+ {/each} +
+
+ + diff --git a/src/lib/ui/node-editors/registry.ts b/src/lib/ui/node-editors/registry.ts new file mode 100644 index 0000000..7c15f15 --- /dev/null +++ b/src/lib/ui/node-editors/registry.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Node editor registry — maps each NodeType to a Svelte editor component. +// +// Each component receives `node: Node` and emits `onupdate: (node: Node) => void`. +// The registry is keyed by the node's config.kind (for task nodes) or node.type +// (for structural nodes), matching the NodeType union. +import type { Node } from '$lib/tasks/model'; +import type { Component } from 'svelte'; + +import CallActivityEditor from './CallActivityEditor.svelte'; +import CallGrpcEditor from './CallGrpcEditor.svelte'; +import CallHttpEditor from './CallHttpEditor.svelte'; +import ForkNodeEditor from './ForkNodeEditor.svelte'; +import ListenTaskEditor from './ListenTaskEditor.svelte'; +import LoopNodeEditor from './LoopNodeEditor.svelte'; +import RaiseTaskEditor from './RaiseTaskEditor.svelte'; +import RunContainerEditor from './RunContainerEditor.svelte'; +import RunScriptEditor from './RunScriptEditor.svelte'; +import RunShellEditor from './RunShellEditor.svelte'; +import RunWorkflowEditor from './RunWorkflowEditor.svelte'; +import SetNodeEditor from './SetNodeEditor.svelte'; +import SwitchNodeEditor from './SwitchNodeEditor.svelte'; +import TryNodeEditor from './TryNodeEditor.svelte'; +import WaitNodeEditor from './WaitNodeEditor.svelte'; + +export interface NodeEditorProps { + node: Node; + onupdate: (node: Node) => void; +} + +// Cast is intentional: each editor declares a narrower Props type internally +// but is used through this common interface. The registry guarantees the correct +// editor is only ever paired with the matching node type. +type EditorComponent = Component; + +const registry: Partial> = { + 'call-activity': CallActivityEditor as unknown as EditorComponent, + 'call-grpc': CallGrpcEditor as unknown as EditorComponent, + 'call-http': CallHttpEditor as unknown as EditorComponent, + 'run-container': RunContainerEditor as unknown as EditorComponent, + 'run-script': RunScriptEditor as unknown as EditorComponent, + 'run-shell': RunShellEditor as unknown as EditorComponent, + 'run-workflow': RunWorkflowEditor as unknown as EditorComponent, + listen: ListenTaskEditor as unknown as EditorComponent, + raise: RaiseTaskEditor as unknown as EditorComponent, + set: SetNodeEditor as unknown as EditorComponent, + wait: WaitNodeEditor as unknown as EditorComponent, + switch: SwitchNodeEditor as unknown as EditorComponent, + fork: ForkNodeEditor as unknown as EditorComponent, + try: TryNodeEditor as unknown as EditorComponent, + loop: LoopNodeEditor as unknown as EditorComponent, +}; + +export function getNodeEditor(node: Node): EditorComponent | null { + const key = node.type === 'task' ? node.config.kind : node.type; + return registry[key] ?? null; +} diff --git a/src/lib/ui/node-editors/set-value.ts b/src/lib/ui/node-editors/set-value.ts new file mode 100644 index 0000000..6dc79ee --- /dev/null +++ b/src/lib/ui/node-editors/set-value.ts @@ -0,0 +1,169 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Pure helpers for parsing and displaying assignment values. +// No UI or Svelte dependencies. +import type { AssignmentValue } from '$lib/tasks/model'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +/** Per-row type-override mode. Not persisted to the IR. */ +export type ValueOverride = 'auto' | 'string' | 'number' | 'boolean' | 'null'; + +export type ParseOk = { ok: true; value: AssignmentValue }; +export type ParseErr = { ok: false; errorKey: string }; +export type ParseResult = ParseOk | ParseErr; + +// --------------------------------------------------------------------------- +// parsePrimitive — AUTO mode +// --------------------------------------------------------------------------- + +/** + * Parse a raw input string as a JSON primitive using AUTO heuristics: + * + * - `"true"` / `"false"` → boolean + * - `"null"` → null + * - JSON string literal `"Hello"` (with quotes) → string without quotes + * - Valid integer or float → number + * - `{...}` or `[...]` that is valid JSON → error (objects/arrays forbidden) + * - Everything else → string (raw input preserved) + */ +export function parsePrimitive(raw: string): ParseResult { + const trimmed = raw.trim(); + + if (trimmed === 'true') return { ok: true, value: true }; + if (trimmed === 'false') return { ok: true, value: false }; + if (trimmed === 'null') return { ok: true, value: null }; + + // Quoted JSON string literal: `"Hello"` → "Hello" (strips surrounding quotes). + if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) { + try { + const parsed = JSON.parse(trimmed); + if (typeof parsed === 'string') return { ok: true, value: parsed }; + } catch { + // Malformed JSON string — fall through to plain string. + } + } + + // Reject JSON objects and arrays explicitly. + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed); + if (typeof parsed === 'object' && parsed !== null) { + return { ok: false, errorKey: 'inspector.set.errorObjectArray' }; + } + } catch { + // Not valid JSON — treat as an ordinary string. + } + } + + // Number: non-empty, finite, and actually parseable as a number. + if (trimmed !== '') { + const num = Number(trimmed); + if (!isNaN(num) && isFinite(num)) { + return { ok: true, value: num }; + } + } + + // String fallback — preserve the original input including any whitespace. + return { ok: true, value: raw }; +} + +// --------------------------------------------------------------------------- +// coerceWithOverride — apply explicit override mode +// --------------------------------------------------------------------------- + +/** + * Parse `raw` according to an explicit override mode. + * + * - `'string'` — always store raw input as a string. + * - `'null'` — always store null; input is ignored. + * - `'number'` — require a valid finite number or return an error. + * - `'boolean'` — accept `true` / `false` (case-insensitive) or return an error. + * - `'auto'` — delegate to `parsePrimitive`. + */ +export function coerceWithOverride( + raw: string, + override: ValueOverride, +): ParseResult { + switch (override) { + case 'string': + return { ok: true, value: raw }; + + case 'null': + return { ok: true, value: null }; + + case 'number': { + const trimmed = raw.trim(); + const num = Number(trimmed); + if (trimmed === '' || isNaN(num) || !isFinite(num)) { + return { ok: false, errorKey: 'inspector.set.errorNotNumber' }; + } + return { ok: true, value: num }; + } + + case 'boolean': { + const lower = raw.trim().toLowerCase(); + if (lower === 'true') return { ok: true, value: true }; + if (lower === 'false') return { ok: true, value: false }; + return { ok: false, errorKey: 'inspector.set.errorNotBoolean' }; + } + + case 'auto': + return parsePrimitive(raw); + } +} + +// --------------------------------------------------------------------------- +// inferOverride — determine initial override from a stored value +// --------------------------------------------------------------------------- + +/** + * Infer a sensible initial override for a stored `AssignmentValue` so that + * displaying → re-parsing the value in the inferred mode produces the same + * stored value (i.e. the display round-trips without type changes). + * + * - `null` → `'null'` + * - boolean → `'auto'` (auto-parse of "true"/"false" gives boolean again) + * - number → `'auto'` (auto-parse of the number string gives number again) + * - string → `'string'` if auto-parse would change the type; else `'auto'` + */ +export function inferOverride(value: AssignmentValue): ValueOverride { + if (value === null) return 'null'; + if (typeof value === 'boolean') return 'auto'; + if (typeof value === 'number') return 'auto'; + + // String: check whether AUTO mode would silently coerce it. + const probe = parsePrimitive(value); + if (!probe.ok || typeof probe.value !== 'string') return 'string'; + return 'auto'; +} + +// --------------------------------------------------------------------------- +// displayValue — convert stored value to input text +// --------------------------------------------------------------------------- + +/** + * Return the text representation of a stored `AssignmentValue` for display + * in a text input. + */ +export function displayValue(value: AssignmentValue): string { + if (value === null) return 'null'; + if (typeof value === 'boolean') return value ? 'true' : 'false'; + if (typeof value === 'number') return String(value); + return value; +} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..34d557f --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { parseAcceptLanguage, resolveLocale } from '$lib/i18n/locales'; + +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = ({ cookies, request }) => { + const cookieLocale = cookies.get('studio_locale'); + + const locale = cookieLocale + ? resolveLocale(cookieLocale) + : parseAcceptLanguage(request.headers.get('accept-language')); + + return { locale }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..6c88b27 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,231 @@ + + + + + + + + +
+
+ {@render children()} +
+
+ +
+
+ + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte deleted file mode 100644 index cc88df0..0000000 --- a/src/routes/+page.svelte +++ /dev/null @@ -1,2 +0,0 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

diff --git a/src/index.test.ts b/src/routes/+page.ts similarity index 69% rename from src/index.test.ts rename to src/routes/+page.ts index 5f11acc..3077de9 100644 --- a/src/index.test.ts +++ b/src/routes/+page.ts @@ -1,5 +1,5 @@ /* - * Copyright 2026 Zigflow authors + * Copyright 2025 - 2026 Zigflow authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { redirect } from '@sveltejs/kit'; -import { describe, expect, it } from 'vitest'; +import type { PageLoad } from './$types'; -describe('sum test', () => { - it('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); +export const load: PageLoad = () => { + redirect(307, '/workflows'); +}; diff --git a/src/routes/api/workflows/[...workflowId]/+server.ts b/src/routes/api/workflows/[...workflowId]/+server.ts new file mode 100644 index 0000000..eff2be3 --- /dev/null +++ b/src/routes/api/workflows/[...workflowId]/+server.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { exportToYaml } from '$lib/export/yaml'; +import { WORKFLOWS_DIR } from '$lib/server/workflows-dir'; +import type { WorkflowFile } from '$lib/tasks/model'; +import { json } from '@sveltejs/kit'; +import { randomUUID } from 'node:crypto'; +import { promises as fs } from 'node:fs'; +import { basename, dirname, resolve } from 'node:path'; + +import type { RequestHandler } from './$types'; + +export const PUT: RequestHandler = async ({ params, request }) => { + const fileName = params.workflowId; + + // Enforce .yaml / .yml extension. + if (!fileName.endsWith('.yaml') && !fileName.endsWith('.yml')) { + return json( + { ok: false, error: 'File must have a .yaml or .yml extension' }, + { status: 400 }, + ); + } + + // Validate path is inside WORKFLOWS_DIR — prevent path traversal. + const filePath = resolve(WORKFLOWS_DIR, fileName); + if (!filePath.startsWith(WORKFLOWS_DIR + '/') && filePath !== WORKFLOWS_DIR) { + return json({ ok: false, error: 'Invalid workflow path' }, { status: 400 }); + } + + // Parse JSON body. + let workflowFile: WorkflowFile; + try { + const body: unknown = await request.json(); + if ( + typeof body !== 'object' || + body === null || + !('workflowFile' in body) + ) { + return json( + { ok: false, error: 'Missing workflowFile in request body' }, + { status: 400 }, + ); + } + workflowFile = (body as { workflowFile: WorkflowFile }).workflowFile; + } catch { + return json({ ok: false, error: 'Invalid JSON body' }, { status: 400 }); + } + + // Export WorkflowFile → YAML (also validates the graph). + const exportResult = exportToYaml(workflowFile); + if (!exportResult.ok) { + return json( + { ok: false, error: `Export failed: ${exportResult.errors.join('; ')}` }, + { status: 400 }, + ); + } + + // Atomic write: write to a temp file then rename so readers never see a + // partial file. + const dir = dirname(filePath); + const tempPath = resolve(dir, `.${basename(filePath)}.tmp.${randomUUID()}`); + try { + await fs.writeFile(tempPath, exportResult.yaml, 'utf-8'); + await fs.rename(tempPath, filePath); + } catch (err) { + // Best-effort cleanup of the temp file. + await fs.unlink(tempPath).catch(() => undefined); + return json( + { ok: false, error: `Write failed: ${String(err)}` }, + { status: 500 }, + ); + } + + return json({ ok: true }); +}; diff --git a/src/routes/workflows/+page.server.ts b/src/routes/workflows/+page.server.ts new file mode 100644 index 0000000..d4d54de --- /dev/null +++ b/src/routes/workflows/+page.server.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { env } from '$env/dynamic/public'; +import { redirect } from '@sveltejs/kit'; +import { promises as fs } from 'fs'; +import yaml from 'js-yaml'; +import { resolve } from 'path'; + +import type { Actions, PageServerLoad } from './$types'; + +const WORKFLOWS_DIR = + env.PUBLIC_WORKFLOWS_DIR ?? resolve(process.cwd(), 'workflows'); + +export const load: PageServerLoad = async () => { + let workflowFiles: string[] = []; + try { + const entries = await fs.readdir(WORKFLOWS_DIR); + workflowFiles = entries + .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml')) + .sort(); + } catch { + // Directory does not exist yet — return empty list. + } + return { workflowFiles }; +}; + +export const actions: Actions = { + default: async ({ request }) => { + const formData = await request.formData(); + const rawName = String(formData.get('name') ?? '').trim(); + if (!rawName) { + return { error: 'Workflow name is required' }; + } + + // Sanitise: lowercase, alphanumeric and hyphens only. + const name = rawName + .toLowerCase() + .replace(/[^a-z0-9-]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + + if (!name) { + return { + error: 'Workflow name must contain at least one valid character', + }; + } + + const fileName = `${name}.yaml`; + const filePath = resolve(WORKFLOWS_DIR, fileName); + + // New format: wrap the primary workflow under its name. + const skeleton = yaml.dump( + { + document: { + dsl: '1.0.0', + namespace: 'default', + name, + version: '0.0.1', + title: name, + }, + do: [{ [name]: { do: [] } }], + }, + { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false }, + ); + + await fs.mkdir(WORKFLOWS_DIR, { recursive: true }); + await fs.writeFile(filePath, skeleton, 'utf-8'); + + redirect(302, `/workflows/${fileName}`); + }, +}; diff --git a/src/routes/workflows/+page.svelte b/src/routes/workflows/+page.svelte new file mode 100644 index 0000000..2cbd08e --- /dev/null +++ b/src/routes/workflows/+page.svelte @@ -0,0 +1,132 @@ + + + + +
+

{t('workflows.title')}

+ + {#if data.workflowFiles.length > 0} + + {:else} +

{t('workflows.empty')}

+ {/if} + +
+ + +
+
+ + diff --git a/src/routes/workflows/[...workflowId]/+page.server.ts b/src/routes/workflows/[...workflowId]/+page.server.ts new file mode 100644 index 0000000..43a3f93 --- /dev/null +++ b/src/routes/workflows/[...workflowId]/+page.server.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { exportToYaml } from '$lib/export/yaml'; +import { WORKFLOWS_DIR } from '$lib/server/workflows-dir'; +import { parseWorkflowFile } from '$lib/tasks/parse'; +import { error } from '@sveltejs/kit'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; + +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params }) => { + const fileName = params.workflowId; + const filePath = resolve(WORKFLOWS_DIR, fileName); + + let content: string; + try { + content = await fs.readFile(filePath, 'utf-8'); + } catch { + throw error(404, `Workflow file not found: ${fileName}`); + } + + let workflowFile, modified; + try { + ({ workflowFile, modified } = parseWorkflowFile(content, fileName)); + } catch (err) { + throw error(400, `Failed to parse workflow "${fileName}": ${err}`); + } + + // Write back immediately if any IDs were generated, so deep links remain + // stable within the same file across server restarts. + if (modified) { + const result = exportToYaml(workflowFile); + if (result.ok) { + await fs.writeFile(filePath, result.yaml, 'utf-8'); + } + } + + return { workflowFile }; +}; diff --git a/src/routes/workflows/[...workflowId]/+page.svelte b/src/routes/workflows/[...workflowId]/+page.svelte new file mode 100644 index 0000000..2ad4afb --- /dev/null +++ b/src/routes/workflows/[...workflowId]/+page.svelte @@ -0,0 +1,1202 @@ + + + + + { + if (e.key === 'Escape' && showExport) showExport = false; + }} +/> + +
+ + + + +
+
+ +
+ + {#if saveStatus === 'saving'} + {t('editor.saving')} + {:else if saveStatus === 'saved'} + {t('editor.saved')} + {:else if saveStatus === 'dirty'} + {t('editor.unsaved')} + {:else if saveStatus === 'error'} + {t('editor.saveFailed')} + {/if} + + {#if saveStatus === 'error'} + + {/if} + +
+
+ + + +
+ {#if currentGraph !== null} + + {:else} +
{t('editor.noGraph')}
+ {/if} + + +
+
+
+ + +{#if showExport} + +{/if} + + diff --git a/src/routes/+layout.ts b/src/routes/workflows/[...workflowId]/+page.ts similarity index 57% rename from src/routes/+layout.ts rename to src/routes/workflows/[...workflowId]/+page.ts index f8eeda3..1a6d3c1 100644 --- a/src/routes/+layout.ts +++ b/src/routes/workflows/[...workflowId]/+page.ts @@ -1,5 +1,5 @@ /* - * Copyright 2026 Zigflow authors + * Copyright 2025 - 2026 Zigflow authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import type { PageLoad } from './$types'; -// Settings useful for statically generated sites -export const prerender = true; -export const ssr = true; -export const trailingSlash = 'always'; +export const load: PageLoad = ({ data, params, url }) => { + const selected = url.searchParams.get('selected'); + const selectedSegments = selected ? selected.split('/').filter(Boolean) : []; + return { + ...data, + workflowId: params.workflowId, + selectedSegments, + }; +}; diff --git a/src/styles/.gitkeep b/src/styles/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/static/favicon.png b/static/favicon.png deleted file mode 100644 index 825b9e6..0000000 Binary files a/static/favicon.png and /dev/null differ diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/svelte.config.js b/svelte.config.js index d3af5b9..5b6b3ac 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,5 +1,5 @@ /* - * Copyright 2026 Zigflow authors + * Copyright 2025 - 2026 Zigflow authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,24 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import adapter from '@sveltejs/adapter-node'; -import adapter from '@sveltejs/adapter-static'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/vite-plugin-svelte').Config} */ +/** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), + compilerOptions: { + runes: true, + }, - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter({ - fallback: '404.html', - }), - }, + kit: { + adapter: adapter(), + }, }; export default config; diff --git a/tests/call-activity-editor.spec.ts b/tests/call-activity-editor.spec.ts new file mode 100644 index 0000000..60a2f34 --- /dev/null +++ b/tests/call-activity-editor.spec.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openActivityInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('run-activity').click(); + // Wait until the Call Activity section is visible. + await expect( + page.getByRole('heading', { name: 'Call Activity' }), + ).toBeVisible({ timeout: 5_000 }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Call Activity editor', () => { + test('selecting a call-activity node shows Activity name and Task queue inputs', async ({ + page, + }) => { + await openActivityInspector(page); + + // Activity name is visible and shows the configured value. + await expect(page.getByLabel('Activity name')).toBeVisible(); + await expect(page.getByLabel('Activity name')).toHaveValue('ProcessOrder'); + + // Task queue is visible and shows the configured value. + await expect(page.getByLabel('Task queue')).toBeVisible(); + await expect(page.getByLabel('Task queue')).toHaveValue('order-processing'); + }); + + test('activity name change persists across reload', async ({ page }) => { + await openActivityInspector(page); + + const nameInput = page.getByLabel('Activity name'); + + // Change activity name. + await nameInput.fill('ShipOrder'); + + // Wait for auto-save. + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // Reload — URL carries ?selected=... so the inspector reopens. + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + // Inspector should reopen on the same node with the updated value. + await expect(page.getByLabel('Activity name')).toHaveValue('ShipOrder', { + timeout: 5_000, + }); + + // Restore original value so the workflow is left in a clean state. + await page.getByLabel('Activity name').fill('ProcessOrder'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('task queue change persists across reload', async ({ page }) => { + await openActivityInspector(page); + + const queueInput = page.getByLabel('Task queue'); + + // Change task queue. + await queueInput.fill('shipping-queue'); + + // Wait for auto-save. + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // Reload. + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + // Inspector should reopen with the updated task queue. + await expect(page.getByLabel('Task queue')).toHaveValue('shipping-queue', { + timeout: 5_000, + }); + + // Verify the exported YAML contains the new task queue. + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yaml = await exportCode.textContent(); + expect(yaml).toContain('shipping-queue'); + await page.getByRole('button', { name: 'Close' }).click(); + + // Restore original value. + await page.getByLabel('Task queue').fill('order-processing'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('empty activity name shows inline warning without blocking editing', async ({ + page, + }) => { + await openActivityInspector(page); + + const nameInput = page.getByLabel('Activity name'); + + // Clear the activity name. + await nameInput.fill(''); + + // Warning should appear. + await expect(page.getByText('Activity name is required')).toBeVisible(); + + // The field should still be editable (not disabled). + await expect(nameInput).toBeEnabled(); + + // Restore original value. + await nameInput.fill('ProcessOrder'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('empty task queue shows inline warning', async ({ page }) => { + await openActivityInspector(page); + + const queueInput = page.getByLabel('Task queue'); + + // Clear the task queue. + await queueInput.fill(''); + + // Warning should appear. + await expect(page.getByText('Task queue is required')).toBeVisible(); + + // Restore original value. + await queueInput.fill('order-processing'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('exported YAML contains call: activity with correct shape', async ({ + page, + }) => { + await openActivityInspector(page); + + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yaml = await exportCode.textContent(); + + // Must use call: activity discriminator. + expect(yaml).toContain('call: activity'); + + // Must include the activity name and task queue. + expect(yaml).toContain('name: ProcessOrder'); + expect(yaml).toContain('taskQueue: order-processing'); + + await page.getByRole('button', { name: 'Close' }).click(); + }); + + test('can add and remove arguments', async ({ page }) => { + await openActivityInspector(page); + + // Click "+ Add argument". + await page.getByRole('button', { name: '+ Add argument' }).click(); + + // Wait for save. + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // There should now be 4 argument value inputs (3 existing + 1 new). + const argInputs = page.getByLabel('Value'); + await expect(argInputs).toHaveCount(4); + + // Remove the newly added argument (last one). + const removeButtons = page.getByLabel('Remove argument'); + await removeButtons.last().click(); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // Back to 3 arguments. + await expect(page.getByLabel('Value')).toHaveCount(3); + }); +}); diff --git a/tests/call-grpc-editor.spec.ts b/tests/call-grpc-editor.spec.ts new file mode 100644 index 0000000..0ab19ab --- /dev/null +++ b/tests/call-grpc-editor.spec.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openGrpcCallInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('grpc-call').click(); + // Wait until the Call gRPC section is visible. + await expect(page.getByRole('heading', { name: 'Call gRPC' })).toBeVisible({ + timeout: 5_000, + }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Call gRPC editor', () => { + test('selecting a call-grpc node shows the Call gRPC section', async ({ + page, + }) => { + await openGrpcCallInspector(page); + + // Proto endpoint is visible and shows the configured value. + await expect(page.getByLabel('Endpoint')).toBeVisible(); + await expect(page.getByLabel('Endpoint')).toHaveValue( + 'api.example.com:8080', + ); + + // Service name is visible and shows the configured value. + await expect(page.getByLabel('Service name')).toBeVisible(); + await expect(page.getByLabel('Service name')).toHaveValue('ExampleService'); + + // Host is visible and shows the configured value. + await expect(page.getByLabel('Host')).toBeVisible(); + await expect(page.getByLabel('Host')).toHaveValue('grpc.example.com'); + + // Port is visible and shows the configured value. + await expect(page.getByLabel('Port')).toBeVisible(); + await expect(page.getByLabel('Port')).toHaveValue('50051'); + + // Method is visible and shows the configured value. + await expect(page.getByLabel('Method')).toBeVisible(); + await expect(page.getByLabel('Method')).toHaveValue('GetData'); + }); + + test('method change persists across reload', async ({ page }) => { + await openGrpcCallInspector(page); + + const methodInput = page.getByLabel('Method'); + + // Change method. + await methodInput.fill('CreateItem'); + + // Wait for auto-save. + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // Reload — URL carries ?selected=... so the inspector reopens. + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + // Inspector should reopen on the same node. + await expect(page.getByLabel('Method')).toHaveValue('CreateItem', { + timeout: 5_000, + }); + + // Restore original value so the workflow is left in a clean state. + await page.getByLabel('Method').fill('GetData'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('invalid port shows a validation error and does not update IR', async ({ + page, + }) => { + await openGrpcCallInspector(page); + + const portInput = page.getByLabel('Port'); + + // Enter an invalid port. + await portInput.fill('abc'); + + // Validation error must appear. + await expect( + page.getByText('Port must be a positive integer'), + ).toBeVisible(); + + // Export YAML: the original port must still be present. + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yaml = await exportCode.textContent(); + expect(yaml).toContain('50051'); + await page.getByRole('button', { name: 'Close' }).click(); + }); + + test('clearing port auto-sets to default 50051 and saves correctly', async ({ + page, + }) => { + await openGrpcCallInspector(page); + + const portInput = page.getByLabel('Port'); + + // Enter a custom port first. + await portInput.fill('9090'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // Clear the port — should default to 50051. + await portInput.fill(''); + + // No validation error. + await expect( + page.getByText('Port must be a positive integer'), + ).not.toBeVisible(); + + // Wait for auto-save. + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // Export YAML: port should be 50051 (the default). + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yaml = await exportCode.textContent(); + expect(yaml).toContain('50051'); + await page.getByRole('button', { name: 'Close' }).click(); + + // Field now shows 50051. + await expect(portInput).toHaveValue('50051'); + }); +}); diff --git a/tests/call-http-editor.spec.ts b/tests/call-http-editor.spec.ts new file mode 100644 index 0000000..3f0befd --- /dev/null +++ b/tests/call-http-editor.spec.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openFetchDataInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('fetch-data').click(); + // Wait until the Call HTTP section is visible. + await expect(page.getByRole('heading', { name: 'Call HTTP' })).toBeVisible({ + timeout: 5_000, + }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Call HTTP editor', () => { + test('selecting a call-http node shows the Call HTTP section', async ({ + page, + }) => { + await openFetchDataInspector(page); + + // Method dropdown is visible and defaults to GET. + await expect(page.getByRole('combobox', { name: 'Method' })).toBeVisible(); + await expect(page.getByRole('combobox', { name: 'Method' })).toHaveValue( + 'get', + ); + + // URL input is visible and shows the configured endpoint. + await expect(page.getByLabel('URL')).toBeVisible(); + await expect(page.getByLabel('URL')).toHaveValue('https://example.com/api'); + }); + + test('method change persists across reload', async ({ page }) => { + await openFetchDataInspector(page); + + const methodSelect = page.getByRole('combobox', { name: 'Method' }); + + // Change method to POST. + await methodSelect.selectOption('post'); + + // Wait for auto-save. + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // Reload — URL carries ?selected=... so the inspector reopens. + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + // Inspector should reopen on the same node. + await expect(page.getByRole('combobox', { name: 'Method' })).toHaveValue( + 'post', + { timeout: 5_000 }, + ); + + // Restore GET so the workflow is left in a clean state. + await page.getByRole('combobox', { name: 'Method' }).selectOption('get'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('clearing URL shows a validation error and does not update IR', async ({ + page, + }) => { + await openFetchDataInspector(page); + + const urlInput = page.getByLabel('URL'); + await urlInput.fill(''); + + // Validation error must appear. + await expect(page.getByText('URL is required')).toBeVisible(); + + // Export YAML: the original endpoint must still be present. + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yaml = await exportCode.textContent(); + expect(yaml).toContain('example.com/api'); + await page.getByRole('button', { name: 'Close' }).click(); + }); +}); diff --git a/tests/i18n.spec.ts b/tests/i18n.spec.ts new file mode 100644 index 0000000..aa5ee14 --- /dev/null +++ b/tests/i18n.spec.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +test.describe('i18n locale switching', () => { + test('locale switcher is present and switching locales does not break the page', async ({ + page, + }) => { + await page.goto(WORKFLOW); + + const localeSelect = page.getByRole('combobox', { name: 'Language' }); + await expect(localeSelect).toBeVisible({ timeout: 5000 }); + + // Switch to British English. + await localeSelect.selectOption('en-GB'); + await expect(localeSelect).toHaveValue('en-GB'); + + // Switch back to International English. + await localeSelect.selectOption('en'); + await expect(localeSelect).toHaveValue('en'); + }); +}); diff --git a/tests/listen-editor.spec.ts b/tests/listen-editor.spec.ts new file mode 100644 index 0000000..78448ca --- /dev/null +++ b/tests/listen-editor.spec.ts @@ -0,0 +1,160 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openListenInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('await-signal').click(); + await expect(page.getByRole('heading', { name: 'Listen' })).toBeVisible({ + timeout: 5_000, + }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Listen editor', () => { + test('selecting a listen node shows Event ID and Type fields', async ({ + page, + }) => { + await openListenInspector(page); + + await expect(page.getByLabel('Event ID')).toBeVisible(); + await expect(page.getByLabel('Event ID')).toHaveValue('order-shipped'); + await expect(page.getByLabel('Type')).toBeVisible(); + await expect(page.getByLabel('Type')).toHaveValue('signal'); + }); + + test('event ID change persists across reload', async ({ page }) => { + await openListenInspector(page); + + await page.getByLabel('Event ID').fill('order-delivered'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + await expect(page.getByLabel('Event ID')).toHaveValue('order-delivered', { + timeout: 5_000, + }); + + // Restore. + await page.getByLabel('Event ID').fill('order-shipped'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('event type change persists across reload', async ({ page }) => { + await openListenInspector(page); + + await page.getByLabel('Type').selectOption('query'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + await expect(page.getByLabel('Type')).toHaveValue('query', { + timeout: 5_000, + }); + + // Restore. + await page.getByLabel('Type').selectOption('signal'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('empty event ID shows inline validation error without blocking editing', async ({ + page, + }) => { + await openListenInspector(page); + + await page.getByLabel('Event ID').fill(''); + + await expect(page.getByText('Event ID is required')).toBeVisible(); + await expect(page.getByLabel('Event ID')).toBeEnabled(); + + // Restore. + await page.getByLabel('Event ID').fill('order-shipped'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('exported YAML contains listen with correct shape', async ({ page }) => { + await openListenInspector(page); + + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yaml = await exportCode.textContent(); + + expect(yaml).toContain('listen:'); + expect(yaml).toContain('order-shipped'); + expect(yaml).toContain('type: signal'); + + await page.getByRole('button', { name: 'Close' }).click(); + }); + + test('accept-if value persists across reload', async ({ page }) => { + await openListenInspector(page); + + await page.getByLabel('Accept if').fill('${ $input.status == "ready" }'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + await expect(page.getByLabel('Accept if')).toHaveValue( + '${ $input.status == "ready" }', + { timeout: 5_000 }, + ); + + // Restore. + await page.getByLabel('Accept if').fill(''); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('can add and remove data entries', async ({ page }) => { + await openListenInspector(page); + + await page.getByRole('button', { name: '+ Add entry' }).click(); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + const keyInputs = page.getByLabel('Key'); + await expect(keyInputs).toHaveCount(1); + + await page.getByRole('button', { name: 'Remove entry' }).click(); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + await expect(page.getByLabel('Key')).toHaveCount(0); + }); +}); diff --git a/tests/loop-inspector.spec.ts b/tests/loop-inspector.spec.ts new file mode 100644 index 0000000..2e06982 --- /dev/null +++ b/tests/loop-inspector.spec.ts @@ -0,0 +1,170 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openLoopInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + // The demo workflow has a loop node named "process-items". + await page.getByText('process-items').click(); + // Wait for the Loop configuration section heading. + await expect( + page.getByRole('heading', { name: 'Loop configuration' }), + ).toBeVisible({ timeout: 5_000 }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Loop inspector', () => { + test('shows all loop fields with correct initial values', async ({ + page, + }) => { + await openLoopInspector(page); + + await expect(page.getByLabel('Collection')).toHaveValue( + '${ $input.items }', + ); + await expect(page.getByLabel('Item variable')).toHaveValue(''); + await expect(page.getByLabel('Index variable')).toHaveValue(''); + await expect(page.getByLabel('Break condition')).toHaveValue(''); + }); + + test('collection change persists across reload', async ({ page }) => { + await openLoopInspector(page); + + await page.getByLabel('Collection').fill('${ $input.data }'); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + + // Reload — URL ?selected= keeps inspector open on the same node. + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await expect(page.getByLabel('Collection')).toHaveValue( + '${ $input.data }', + { + timeout: 5_000, + }, + ); + + // Restore original value. + await page.getByLabel('Collection').fill('${ $input.items }'); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + }); + + test('collection change is reflected in YAML export', async ({ page }) => { + await openLoopInspector(page); + + await page.getByLabel('Collection').fill('${ $context.list }'); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + await expect(exportCode).toContainText('$context.list'); + await page.getByRole('button', { name: 'Close' }).click(); + + // Restore. + await page.getByLabel('Collection').fill('${ $input.items }'); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + }); + + test('optional fields (item variable, index variable) persist', async ({ + page, + }) => { + await openLoopInspector(page); + + await page.getByLabel('Item variable').fill('item'); + await page.getByLabel('Index variable').fill('idx'); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await expect(page.getByLabel('Item variable')).toHaveValue('item', { + timeout: 5_000, + }); + await expect(page.getByLabel('Index variable')).toHaveValue('idx', { + timeout: 5_000, + }); + + // Restore. + await page.getByLabel('Item variable').fill(''); + await page.getByLabel('Index variable').fill(''); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + }); + + test('empty collection shows a validation warning', async ({ page }) => { + await openLoopInspector(page); + + await page.getByLabel('Collection').fill(''); + await expect(page.getByText('Collection is required')).toBeVisible(); + }); + + test('invalid identifier in item variable shows a warning', async ({ + page, + }) => { + await openLoopInspector(page); + + await page.getByLabel('Item variable').fill('invalid identifier'); + await expect(page.getByText('Must be a valid identifier')).toBeVisible(); + + // Restore. + await page.getByLabel('Item variable').fill(''); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + }); + + test('"Enter loop body" button navigates into the body graph', async ({ + page, + }) => { + await openLoopInspector(page); + + await page.getByRole('button', { name: 'Enter loop body' }).click(); + + // After navigating in, the inspector should close (no node selected). + await expect(page.getByLabel('Collection')).toHaveCount(0); + // Breadcrumb should show we are inside the loop body. + await expect(page.locator('.breadcrumb')).toContainText('process-items'); + }); +}); diff --git a/tests/run-container-editor.spec.ts b/tests/run-container-editor.spec.ts new file mode 100644 index 0000000..c56cf3c --- /dev/null +++ b/tests/run-container-editor.spec.ts @@ -0,0 +1,114 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openContainerInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('run-container-demo').click(); + await expect( + page.getByRole('heading', { name: 'Run Container' }), + ).toBeVisible({ timeout: 5_000 }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Run Container editor', () => { + test('selecting a run-container node shows the inspector with correct title', async ({ + page, + }) => { + await openContainerInspector(page); + + await expect(page.getByLabel('Image')).toBeVisible(); + await expect(page.getByLabel('Image')).toHaveValue('alpine:latest'); + }); + + test('image change persists across reload', async ({ page }) => { + await openContainerInspector(page); + + const imageInput = page.getByLabel('Image'); + await imageInput.fill('ubuntu:22.04'); + + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + await expect(page.getByLabel('Image')).toHaveValue('ubuntu:22.04', { + timeout: 5_000, + }); + + // Restore original value. + await page.getByLabel('Image').fill('alpine:latest'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('empty image shows inline error', async ({ page }) => { + await openContainerInspector(page); + + const imageInput = page.getByLabel('Image'); + await imageInput.fill(''); + + await expect(page.getByText('Image is required')).toBeVisible(); + await expect(imageInput).toBeEnabled(); + + // Restore original value. + await imageInput.fill('alpine:latest'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('exported YAML contains run: container with correct shape', async ({ + page, + }) => { + await openContainerInspector(page); + + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yamlText = await exportCode.textContent(); + + expect(yamlText).toContain('container:'); + expect(yamlText).toContain('image: alpine:latest'); + + await page.getByRole('button', { name: 'Close' }).click(); + }); + + test('can add and remove arguments', async ({ page }) => { + await openContainerInspector(page); + + await page.getByRole('button', { name: '+ Add argument' }).first().click(); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // Remove the newly added argument. + const removeButtons = page.getByLabel('Remove argument'); + await removeButtons.last().click(); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); +}); diff --git a/tests/run-script-editor.spec.ts b/tests/run-script-editor.spec.ts new file mode 100644 index 0000000..e423703 --- /dev/null +++ b/tests/run-script-editor.spec.ts @@ -0,0 +1,113 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openScriptInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('run-script-demo').click(); + await expect(page.getByRole('heading', { name: 'Run Script' })).toBeVisible({ + timeout: 5_000, + }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Run Script editor', () => { + test('selecting a run-script node shows the inspector with correct title', async ({ + page, + }) => { + await openScriptInspector(page); + + await expect(page.getByLabel('Language')).toBeVisible(); + await expect(page.getByLabel('Inline code')).toBeVisible(); + }); + + test('shows await-locked note', async ({ page }) => { + await openScriptInspector(page); + + await expect( + page.getByText('Scripts always await completion.'), + ).toBeVisible(); + }); + + test('code change persists across reload', async ({ page }) => { + await openScriptInspector(page); + + const codeArea = page.getByLabel('Inline code'); + await codeArea.fill('console.log("updated");'); + + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + await expect(page.getByLabel('Inline code')).toHaveValue( + 'console.log("updated");', + { timeout: 5_000 }, + ); + + // Restore original value. + await page + .getByLabel('Inline code') + .fill("console.log('hello from script');\n"); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('empty code shows inline error', async ({ page }) => { + await openScriptInspector(page); + + const codeArea = page.getByLabel('Inline code'); + await codeArea.fill(''); + + await expect(page.getByText('Code is required')).toBeVisible(); + await expect(codeArea).toBeEnabled(); + + // Restore original value. + await codeArea.fill("console.log('hello from script');\n"); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('exported YAML contains run: script with correct shape', async ({ + page, + }) => { + await openScriptInspector(page); + + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yamlText = await exportCode.textContent(); + + expect(yamlText).toContain('script:'); + expect(yamlText).toContain('language: js'); + + await page.getByRole('button', { name: 'Close' }).click(); + }); +}); diff --git a/tests/run-shell-editor.spec.ts b/tests/run-shell-editor.spec.ts new file mode 100644 index 0000000..6f26a16 --- /dev/null +++ b/tests/run-shell-editor.spec.ts @@ -0,0 +1,114 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openShellInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('run-shell-demo').click(); + await expect(page.getByRole('heading', { name: 'Run Shell' })).toBeVisible({ + timeout: 5_000, + }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Run Shell editor', () => { + test('selecting a run-shell node shows the inspector with correct title', async ({ + page, + }) => { + await openShellInspector(page); + + await expect(page.getByLabel('Command')).toBeVisible(); + await expect(page.getByLabel('Command')).toHaveValue('echo hello'); + }); + + test('command change persists across reload', async ({ page }) => { + await openShellInspector(page); + + const commandInput = page.getByLabel('Command'); + await commandInput.fill('ls -la'); + + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + await expect(page.getByLabel('Command')).toHaveValue('ls -la', { + timeout: 5_000, + }); + + // Restore original value. + await page.getByLabel('Command').fill('echo hello'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('empty command shows inline error', async ({ page }) => { + await openShellInspector(page); + + const commandInput = page.getByLabel('Command'); + await commandInput.fill(''); + + await expect(page.getByText('Command is required')).toBeVisible(); + await expect(commandInput).toBeEnabled(); + + // Restore original value. + await commandInput.fill('echo hello'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('exported YAML contains run: shell with correct shape', async ({ + page, + }) => { + await openShellInspector(page); + + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yamlText = await exportCode.textContent(); + + expect(yamlText).toContain('shell:'); + expect(yamlText).toContain('command: echo hello'); + + await page.getByRole('button', { name: 'Close' }).click(); + }); + + test('can add and remove arguments', async ({ page }) => { + await openShellInspector(page); + + await page.getByRole('button', { name: '+ Add argument' }).first().click(); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + // Remove the newly added argument. + const removeButtons = page.getByLabel('Remove argument'); + await removeButtons.last().click(); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); +}); diff --git a/tests/run-workflow-editor.spec.ts b/tests/run-workflow-editor.spec.ts new file mode 100644 index 0000000..14d21c5 --- /dev/null +++ b/tests/run-workflow-editor.spec.ts @@ -0,0 +1,126 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openWorkflowInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('run-workflow-demo').click(); + await expect(page.getByRole('heading', { name: 'Run Workflow' })).toBeVisible( + { timeout: 5_000 }, + ); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Run Workflow editor', () => { + test('selecting a run-workflow node shows the inspector with correct title', async ({ + page, + }) => { + await openWorkflowInspector(page); + + await expect(page.getByLabel('Workflow name')).toBeVisible(); + await expect(page.getByLabel('Workflow name')).toHaveValue( + 'child-workflow', + ); + await expect(page.getByLabel('Namespace')).toHaveValue('default'); + await expect(page.getByLabel('Version')).toHaveValue('0.0.1'); + }); + + test('workflow name change persists across reload', async ({ page }) => { + await openWorkflowInspector(page); + + const nameInput = page.getByLabel('Workflow name'); + await nameInput.fill('updated-workflow'); + + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + await expect(page.getByLabel('Workflow name')).toHaveValue( + 'updated-workflow', + { timeout: 5_000 }, + ); + + // Restore original value. + await page.getByLabel('Workflow name').fill('child-workflow'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('empty workflow name shows inline error', async ({ page }) => { + await openWorkflowInspector(page); + + const nameInput = page.getByLabel('Workflow name'); + await nameInput.fill(''); + + await expect(page.getByText('Workflow name is required')).toBeVisible(); + await expect(nameInput).toBeEnabled(); + + // Restore original value. + await nameInput.fill('child-workflow'); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); + + test('exported YAML contains run: workflow with correct shape', async ({ + page, + }) => { + await openWorkflowInspector(page); + + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const yamlText = await exportCode.textContent(); + + expect(yamlText).toContain('workflow:'); + expect(yamlText).toContain('name: child-workflow'); + expect(yamlText).toContain('namespace: default'); + expect(yamlText).toContain('version: 0.0.1'); + + await page.getByRole('button', { name: 'Close' }).click(); + }); + + test('unchecking await shows help text', async ({ page }) => { + await openWorkflowInspector(page); + + const awaitCheckbox = page.getByLabel('Await result'); + await awaitCheckbox.uncheck(); + + await expect( + page.getByText( + 'When disabled, the child workflow starts and the parent continues immediately', + ), + ).toBeVisible(); + + // Restore: check it again. + await awaitCheckbox.check(); + await expect(page.getByText('Saved')).toBeVisible({ timeout: 5_000 }); + }); +}); diff --git a/tests/set-assignment-values.spec.ts b/tests/set-assignment-values.spec.ts new file mode 100644 index 0000000..5e4c156 --- /dev/null +++ b/tests/set-assignment-values.spec.ts @@ -0,0 +1,216 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Open the greet node inspector (it is a Set node in the demo workflow). + * Waits until the Assignments section is visible. + */ +async function openGreetInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('greet').click(); + await expect( + page.getByRole('button', { name: '+ Add assignment' }), + ).toBeVisible({ timeout: 5_000 }); +} + +/** + * Add a new assignment row, set its key and value (both cleared and re-typed + * so the test is idempotent across re-runs), and return the new row locator. + */ +async function addRow(page: Page, key: string, value: string) { + const addBtn = page.getByRole('button', { name: '+ Add assignment' }); + const rows = page.locator('.assignment-item'); + const countBefore = await rows.count(); + + await addBtn.click(); + await expect(rows).toHaveCount(countBefore + 1); + + const newRow = rows.last(); + const keyInput = newRow.getByRole('textbox').first(); + await keyInput.fill(key); + + const valueInput = newRow.getByLabel('Value'); + await valueInput.fill(value); + + return newRow; +} + +/** + * Export YAML and return the full text of the export code block. + */ +async function exportYaml(page: Page): Promise { + await page.getByRole('button', { name: 'Export YAML' }).click(); + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + const text = await exportCode.textContent(); + // Close the dialog so it doesn't interfere with other assertions. + await page.getByRole('button', { name: 'Close' }).click(); + return text ?? ''; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Set assignment value types', () => { + test('auto mode: boolean true — YAML emits unquoted true', async ({ + page, + }) => { + await openGreetInspector(page); + await addRow(page, 'pw-bool', 'true'); + + const yaml = await exportYaml(page); + // js-yaml emits boolean true without quotes. + expect(yaml).toMatch(/pw-bool:\s+true(?!\w)/); + // Must NOT be quoted. + expect(yaml).not.toMatch(/pw-bool:\s+'true'/); + expect(yaml).not.toMatch(/pw-bool:\s+"true"/); + }); + + test('auto mode: number — YAML emits unquoted integer', async ({ page }) => { + await openGreetInspector(page); + await addRow(page, 'pw-num', '123'); + + const yaml = await exportYaml(page); + expect(yaml).toMatch(/pw-num:\s+123(?!\d)/); + // Must NOT be a quoted string. + expect(yaml).not.toMatch(/pw-num:\s+'123'/); + }); + + test('auto mode: plain string — YAML emits unquoted string', async ({ + page, + }) => { + await openGreetInspector(page); + await addRow(page, 'pw-str', 'hello'); + + const yaml = await exportYaml(page); + expect(yaml).toMatch(/pw-str:\s+hello/); + }); + + test('auto mode: object input — shows validation error, YAML unchanged', async ({ + page, + }) => { + await openGreetInspector(page); + + const addBtn = page.getByRole('button', { name: '+ Add assignment' }); + const rows = page.locator('.assignment-item'); + const countBefore = await rows.count(); + + await addBtn.click(); + await expect(rows).toHaveCount(countBefore + 1); + + const newRow = rows.last(); + + // Set a stable key first. + await newRow.getByRole('textbox').first().fill('pw-obj'); + + // Type an object into the value input. + const valueInput = newRow.getByLabel('Value'); + await valueInput.fill('{ "a": 1 }'); + + // Error message must be visible. + await expect( + page.getByText('Objects and arrays are not supported'), + ).toBeVisible(); + + // YAML export must NOT contain pw-obj (row was never persisted with object). + const yaml = await exportYaml(page); + expect(yaml).not.toContain('pw-obj:'); + }); + + test('string override: "true" is stored as quoted string in YAML', async ({ + page, + }) => { + await openGreetInspector(page); + + const addBtn = page.getByRole('button', { name: '+ Add assignment' }); + const rows = page.locator('.assignment-item'); + const countBefore = await rows.count(); + + await addBtn.click(); + await expect(rows).toHaveCount(countBefore + 1); + + const newRow = rows.last(); + await newRow.getByRole('textbox').first().fill('pw-strover'); + + // Switch override to String before entering the value. + await newRow + .getByRole('combobox', { name: /Type for/ }) + .selectOption('string'); + + const valueInput = newRow.getByLabel('Value'); + await valueInput.fill('true'); + + const yaml = await exportYaml(page); + // js-yaml must quote the string "true" to distinguish it from boolean. + expect(yaml).toMatch(/pw-strover:\s+'true'/); + }); + + test('auto mode: float — YAML emits unquoted float', async ({ page }) => { + await openGreetInspector(page); + await addRow(page, 'pw-float', '3.14'); + + const yaml = await exportYaml(page); + expect(yaml).toMatch(/pw-float:\s+3\.14/); + }); + + test('auto mode: null — YAML emits null', async ({ page }) => { + await openGreetInspector(page); + await addRow(page, 'pw-null-auto', 'null'); + + const yaml = await exportYaml(page); + expect(yaml).toMatch(/pw-null-auto:\s+null/); + }); + + test('null override — value input is disabled and YAML emits null', async ({ + page, + }) => { + await openGreetInspector(page); + + const addBtn = page.getByRole('button', { name: '+ Add assignment' }); + const rows = page.locator('.assignment-item'); + const countBefore = await rows.count(); + + await addBtn.click(); + await expect(rows).toHaveCount(countBefore + 1); + + const newRow = rows.last(); + await newRow.getByRole('textbox').first().fill('pw-nullov'); + + // Switch override to Null. + await newRow + .getByRole('combobox', { name: /Type for/ }) + .selectOption('null'); + + // Value input must be disabled. + const valueInput = newRow.getByLabel('Value'); + await expect(valueInput).toBeDisabled(); + + const yaml = await exportYaml(page); + expect(yaml).toMatch(/pw-nullov:\s+null/); + }); +}); diff --git a/tests/switch-inspector.spec.ts b/tests/switch-inspector.spec.ts new file mode 100644 index 0000000..23cdf00 --- /dev/null +++ b/tests/switch-inspector.spec.ts @@ -0,0 +1,208 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type Page, expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function openSwitchInspector(page: Page) { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + // The demo workflow has a switch node named "route". + await page.getByText('route').click(); + // Wait until the Branches section is visible. + await expect( + page.locator('[data-testid="switch-branch-card"]').first(), + ).toBeVisible({ timeout: 5_000 }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe('Switch inspector', () => { + test('shows editable branch fields for each branch', async ({ page }) => { + await openSwitchInspector(page); + + const cards = page.locator('[data-testid="switch-branch-card"]'); + await expect(cards).toHaveCount(2); + + // First branch (fast-path): has name, condition, and target fields. + const first = cards.first(); + await expect(first.getByLabel('Branch name')).toHaveValue('fast-path'); + await expect(first.getByLabel('Condition (when)')).toHaveValue( + '${ $input.fast == true }', + ); + await expect(first.getByLabel('Target workflow')).toHaveValue( + 'route-fast-path', + ); + + // Second branch (default): has name and target but no condition field. + const second = cards.nth(1); + await expect(second.getByLabel('Branch name')).toHaveValue('default'); + await expect(second.getByLabel('Condition (when)')).toHaveCount(0); + await expect(second.getByLabel('Target workflow')).toHaveValue( + 'route-default', + ); + // Default badge is shown. + await expect(second.getByText('Default branch')).toBeVisible(); + }); + + test('can rename a branch and change its condition', async ({ page }) => { + await openSwitchInspector(page); + + const first = page.locator('[data-testid="switch-branch-card"]').first(); + const nameInput = first.getByLabel('Branch name'); + const condInput = first.getByLabel('Condition (when)'); + + // Rename the branch. + await nameInput.fill('fast-route'); + // Change the condition. + await condInput.fill('${ $input.speed == "fast" }'); + + // Wait for auto-save. + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + + // Reload and verify persistence. + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('route').click(); + await expect( + page.locator('[data-testid="switch-branch-card"]').first(), + ).toBeVisible({ timeout: 5_000 }); + + const firstAfter = page + .locator('[data-testid="switch-branch-card"]') + .first(); + await expect(firstAfter.getByLabel('Branch name')).toHaveValue( + 'fast-route', + ); + await expect(firstAfter.getByLabel('Condition (when)')).toHaveValue( + '${ $input.speed == "fast" }', + ); + + // Restore original values. + await firstAfter.getByLabel('Branch name').fill('fast-path'); + await firstAfter + .getByLabel('Condition (when)') + .fill('${ $input.fast == true }'); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + }); + + test('can change target workflow and it persists', async ({ page }) => { + await openSwitchInspector(page); + + const first = page.locator('[data-testid="switch-branch-card"]').first(); + const targetSelect = first.getByLabel('Target workflow'); + + // Change target to route-default. + await targetSelect.selectOption('route-default'); + + // Wait for auto-save. + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + + // Reload and verify. + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await page.getByText('route').click(); + await expect( + page.locator('[data-testid="switch-branch-card"]').first(), + ).toBeVisible({ timeout: 5_000 }); + + await expect( + page + .locator('[data-testid="switch-branch-card"]') + .first() + .getByLabel('Target workflow'), + ).toHaveValue('route-default'); + + // Restore original. + await page + .locator('[data-testid="switch-branch-card"]') + .first() + .getByLabel('Target workflow') + .selectOption('route-fast-path'); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + }); + + test('can add a new branch', async ({ page }) => { + await openSwitchInspector(page); + + const initialCount = await page + .locator('[data-testid="switch-branch-card"]') + .count(); + + // Click the add branch button. + await page.getByRole('button', { name: '+ Add branch' }).click(); + + // A new card should appear. + await expect( + page.locator('[data-testid="switch-branch-card"]'), + ).toHaveCount(initialCount + 1); + + // The new branch should have "new-branch" as the name. + const last = page.locator('[data-testid="switch-branch-card"]').last(); + await expect(last.getByLabel('Branch name')).toHaveValue('new-branch'); + + // New non-default branch should have a condition field. + await expect(last.getByLabel('Condition (when)')).toBeVisible(); + + // Condition is empty — warning should be shown. + await expect(last.getByText('Condition is required')).toBeVisible(); + + // Wait for save, then delete the new branch to restore state. + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + await last + .getByRole('button', { name: 'Remove branch new-branch' }) + .click(); + await expect(page.getByTestId('save-status')).toContainText('Saved', { + timeout: 5_000, + }); + }); + + test('"Add default branch" button is hidden when default already exists', async ({ + page, + }) => { + await openSwitchInspector(page); + + // The demo workflow already has a default branch — button should not appear. + await expect( + page.getByRole('button', { name: '+ Add default branch' }), + ).toHaveCount(0); + }); +}); diff --git a/tests/workflow-delete.spec.ts b/tests/workflow-delete.spec.ts new file mode 100644 index 0000000..5dc567a --- /dev/null +++ b/tests/workflow-delete.spec.ts @@ -0,0 +1,169 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +test.describe('Workflow deletion', () => { + test('can delete a workflow and sidebar updates immediately', async ({ + page, + }) => { + await page.goto(WORKFLOW); + + // Add a second workflow so we have two. + await page.getByRole('button', { name: '+ Add workflow' }).click(); + + // Wait for the save to complete before continuing. + const saveStatus = page.getByTestId('save-status'); + await expect(saveStatus).toContainText('Saved', { timeout: 5000 }); + + // Sidebar should now show two workflow entries. + const workflowItems = page.locator('.workflow-list-item'); + await expect(workflowItems).toHaveCount(2); + + // Hover over the second workflow item to reveal the delete button, then click it. + const secondItem = workflowItems.nth(1); + await secondItem.hover(); + await secondItem.getByRole('button', { name: 'Delete workflow' }).click(); + + // Confirmation dialog should appear. + const dialog = page.getByRole('dialog', { name: 'Delete workflow' }); + await expect(dialog).toBeVisible(); + await expect(dialog).toContainText( + 'Are you sure you want to delete this workflow?', + ); + + // Confirm the deletion. + await dialog.getByRole('button', { name: 'Delete' }).click(); + + // Dialog should close and sidebar should show only one workflow entry. + await expect(dialog).not.toBeVisible(); + await expect(workflowItems).toHaveCount(1); + + // Wait for the save triggered by the deletion to complete. + await expect(saveStatus).toContainText('Saved', { timeout: 5000 }); + }); + + test('deleted workflow does not reappear after reload', async ({ page }) => { + await page.goto(WORKFLOW); + + // Add a second workflow. + await page.getByRole('button', { name: '+ Add workflow' }).click(); + const saveStatus = page.getByTestId('save-status'); + await expect(saveStatus).toContainText('Saved', { timeout: 5000 }); + + // Record the name of the second workflow before deleting it. + const workflowItems = page.locator('.workflow-list-item'); + const secondItem = workflowItems.nth(1); + const secondName = await secondItem.locator('.workflow-item').textContent(); + + // Delete the second workflow. + await secondItem.hover(); + await secondItem.getByRole('button', { name: 'Delete workflow' }).click(); + await page + .getByRole('dialog', { name: 'Delete workflow' }) + .getByRole('button', { name: 'Delete' }) + .click(); + + // Wait for save. + await expect(saveStatus).toContainText('Saved', { timeout: 5000 }); + + // Reload the page and confirm the deleted workflow is gone. + await page.reload(); + await expect(workflowItems).toHaveCount(1); + if (secondName) { + await expect(page.locator('.workflow-item')).not.toContainText( + secondName.trim(), + ); + } + }); + + test('delete is blocked when only one workflow remains', async ({ page }) => { + await page.goto(WORKFLOW); + + // With only one workflow, the delete button should be disabled. + const workflowItems = page.locator('.workflow-list-item'); + await expect(workflowItems).toHaveCount(1); + + const soleItem = workflowItems.first(); + await soleItem.hover(); + + const deleteBtn = soleItem.getByRole('button', { name: 'Delete workflow' }); + await expect(deleteBtn).toBeDisabled(); + + // No confirmation dialog should open. + await deleteBtn.click({ force: true }); + await expect( + page.getByRole('dialog', { name: 'Delete workflow' }), + ).not.toBeVisible(); + }); + + test('cancelling deletion leaves the workflow intact', async ({ page }) => { + await page.goto(WORKFLOW); + + // Add a second workflow. + await page.getByRole('button', { name: '+ Add workflow' }).click(); + const saveStatus = page.getByTestId('save-status'); + await expect(saveStatus).toContainText('Saved', { timeout: 5000 }); + + const workflowItems = page.locator('.workflow-list-item'); + + // Open the delete dialog for the second workflow. + const secondItem = workflowItems.nth(1); + await secondItem.hover(); + await secondItem.getByRole('button', { name: 'Delete workflow' }).click(); + + const dialog = page.getByRole('dialog', { name: 'Delete workflow' }); + await expect(dialog).toBeVisible(); + + // Click Cancel. + await dialog.getByRole('button', { name: 'Cancel' }).click(); + + // Dialog is gone, both workflows still present. + await expect(dialog).not.toBeVisible(); + await expect(workflowItems).toHaveCount(2); + }); + + test('deleting the active workflow navigates to another', async ({ + page, + }) => { + await page.goto(WORKFLOW); + + // Add a second workflow and select it. + await page.getByRole('button', { name: '+ Add workflow' }).click(); + const saveStatus = page.getByTestId('save-status'); + await expect(saveStatus).toContainText('Saved', { timeout: 5000 }); + + const workflowItems = page.locator('.workflow-list-item'); + // Select the second workflow. + await workflowItems.nth(1).locator('.workflow-item').click(); + + // Delete the currently-selected second workflow. + const secondItem = workflowItems.nth(1); + await secondItem.hover(); + await secondItem.getByRole('button', { name: 'Delete workflow' }).click(); + await page + .getByRole('dialog', { name: 'Delete workflow' }) + .getByRole('button', { name: 'Delete' }) + .click(); + + // App should have navigated to the first (remaining) workflow. + await expect(workflowItems).toHaveCount(1); + await expect(workflowItems.first().locator('.workflow-item')).toHaveClass( + /workflow-item--selected/, + ); + }); +}); diff --git a/tests/workflow-navigation.spec.ts b/tests/workflow-navigation.spec.ts new file mode 100644 index 0000000..91d6afc --- /dev/null +++ b/tests/workflow-navigation.spec.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +test.describe('Workflow navigation invariants', () => { + test('selecting a root node updates query param', async ({ page }) => { + await page.goto(WORKFLOW); + + await page.getByText('greet').click(); + + const url = new URL(page.url()); + const selected = url.searchParams.get('selected'); + + expect(selected).toBeTruthy(); + expect(selected?.split('/').length).toBe(1); + }); + + test('refresh preserves root selection', async ({ page }) => { + await page.goto(WORKFLOW); + + await page.getByText('greet').click(); + const firstUrl = page.url(); + + await page.reload(); + await expect(page).toHaveURL(firstUrl); + + await expect( + page.locator('.flow-node-name', { hasText: 'greet' }), + ).toHaveAttribute('data-selected', 'true'); + + // Inspector should also be open after refresh with a selected node. + await expect(page.locator('.inspector-name')).toHaveValue('greet'); + }); + + test('selecting switch branch updates query param', async ({ page }) => { + await page.goto(WORKFLOW); + + await page.getByText('route').click(); + await page.getByText('fast-path').click(); + + const url = new URL(page.url()); + const selected = url.searchParams.get('selected'); + + expect(selected).toBeTruthy(); + expect(selected?.split('/').length).toBe(2); + }); + + test('deep link restores branch context', async ({ page }) => { + await page.goto(WORKFLOW); + + await page.getByText('route').click(); + await page.getByText('fast-path').click(); + + const deepLink = page.url(); + + const newPage = await page.context().newPage(); + await newPage.goto(deepLink); + + await expect(newPage).toHaveURL(deepLink); + await expect(newPage.getByText(/Editing/i)).toContainText('fast-path'); + }); + + test('browser back restores previous selection', async ({ page }) => { + await page.goto(WORKFLOW); + + await page.getByText('greet').click(); + const firstUrl = page.url(); + + await page.getByText('pause').click(); + const secondUrl = page.url(); + + expect(firstUrl).not.toBe(secondUrl); + + await page.goBack(); + + await expect(page).toHaveURL(firstUrl); + await expect( + page.locator('.flow-node-name', { hasText: 'greet' }), + ).toHaveAttribute('data-selected', 'true'); + + // Inspector should reopen after popstate restores a selected node. + await expect(page.locator('.inspector-name')).toHaveValue('greet'); + }); + + test('entering loop body updates URL to loopNodeId/body', async ({ + page, + }) => { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + // Click the loop body nav row on the "process-items" loop node. + await page.getByText('process-items').click(); + await page.getByRole('button', { name: 'Enter loop body' }).click(); + + const url = new URL(page.url()); + const selected = url.searchParams.get('selected'); + + expect(selected).toBeTruthy(); + const segments = selected!.split('/'); + expect(segments.length).toBe(2); + expect(segments[1]).toBe('body'); + }); + + test('loop body deep link renders body graph on refresh', async ({ + page, + }) => { + await page.goto(WORKFLOW); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + // Navigate into the loop body. + await page.getByText('process-items').click(); + await page.getByRole('button', { name: 'Enter loop body' }).click(); + const deepLink = page.url(); + + // Reload at the deep link and confirm we are still inside the body. + await page.reload(); + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + await expect(page).toHaveURL(deepLink); + + // Breadcrumb should include the loop node name. + await expect(page.locator('.breadcrumb')).toContainText('process-items'); + }); +}); diff --git a/tests/workflow-save.spec.ts b/tests/workflow-save.spec.ts new file mode 100644 index 0000000..a9c7954 --- /dev/null +++ b/tests/workflow-save.spec.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +test.describe('Workflow save', () => { + test('dragging a palette item makes the editor dirty then saved, and persists after reload', async ({ + page, + }) => { + await page.goto(WORKFLOW); + + // Wait for the canvas to be fully hydrated: nodes are shown with dimensions + // only after the client-side $effect runs, so we wait for a canvas node to + // appear before triggering any drag. + await page + .locator('.svelte-flow__node') + .first() + .waitFor({ state: 'visible', timeout: 10_000 }); + + // Locate the "Wait" palette item and the canvas drop target. + const waitItem = page.getByText('Wait', { exact: true }); + const canvas = page.getByRole('region', { name: 'Workflow canvas' }); + + await expect(waitItem).toBeVisible(); + await expect(canvas).toBeVisible(); + + // Use Playwright's native dragTo so the browser fires trusted drag events + // and the dataTransfer set in ondragstart flows through to ondrop correctly. + await waitItem.dragTo(canvas); + + // Status should transition through dirty / saving to saved. + const saveStatus = page.getByTestId('save-status'); + await expect(saveStatus).toContainText(/Unsaved changes|Saving…/); + await expect(saveStatus).toContainText('Saved', { timeout: 5000 }); + + // After reload the newly inserted "wait" node must persist. + await page.reload(); + + await page.getByRole('button', { name: 'Export YAML' }).click(); + + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + + const yamlText = await exportCode.textContent(); + // The wait task config serialises as "wait:\n seconds: ..." in YAML. + expect(yamlText).toMatch(/wait:\s+seconds:/); + }); +}); diff --git a/tests/yaml-export-shape.spec.ts b/tests/yaml-export-shape.spec.ts new file mode 100644 index 0000000..bbd2d06 --- /dev/null +++ b/tests/yaml-export-shape.spec.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2025 - 2026 Zigflow authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, test } from '@playwright/test'; + +const WORKFLOW = '/workflows/demo-workflow.yaml'; + +test.describe('YAML export shape', () => { + test('exported YAML uses workflows-list format', async ({ page }) => { + await page.goto(WORKFLOW); + + await page.getByRole('button', { name: 'Export YAML' }).click(); + + const exportCode = page.locator('.export-code'); + await expect(exportCode).toBeVisible(); + + const text = await exportCode.textContent(); + expect(text).toBeTruthy(); + + // Top-level do: must contain named workflow declarations, not bare steps. + expect(text).toMatch(/- demo-workflow:\s+do:/); + + // Hoisted sub-workflows must appear as top-level workflow entries. + expect(text).toMatch(/- route-fast-path:\s+do:/); + expect(text).toMatch(/- route-default:\s+do:/); + + // Switch branches must use then: references, not inline steps. + expect(text).toContain('then: route-fast-path'); + expect(text).toContain('then: route-default'); + + // Primary workflow steps must be nested under demo-workflow.do. + expect(text).toContain('greet:'); + expect(text).toContain('pause:'); + expect(text).toContain('route:'); + }); + + test('sidebar lists all top-level workflows', async ({ page }) => { + await page.goto(WORKFLOW); + + // All three workflows from the new format should be in the sidebar. + await expect( + page.getByRole('button', { name: 'demo-workflow' }), + ).toBeVisible(); + await expect( + page.getByRole('button', { name: 'route-fast-path' }), + ).toBeVisible(); + await expect( + page.getByRole('button', { name: 'route-default' }), + ).toBeVisible(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 4344710..bbeba3a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { + "rewriteRelativeImportExtensions": true, "allowJs": true, "checkJs": true, "esModuleInterop": true, diff --git a/vite.config.ts b/vite.config.ts index 216663b..dea8d65 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,5 @@ /* - * Copyright 2026 Zigflow authors + * Copyright 2025 - 2026 Zigflow authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vitest/config'; +import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()], - server: { - host: process.env.VITE_HOST, - port: Number(process.env.VITE_PORT ?? 5173), - }, - test: { - include: ['src/**/*.{test,spec}.{js,ts}'], - }, + plugins: [sveltekit()], + server: { + host: process.env.HOST ?? '0.0.0.0', + port: Number(process.env.PORT ?? 5173), + }, }); diff --git a/workflows/demo-workflow.yaml b/workflows/demo-workflow.yaml new file mode 100644 index 0000000..3a46809 --- /dev/null +++ b/workflows/demo-workflow.yaml @@ -0,0 +1,74 @@ +document: + dsl: 1.0.0 + namespace: demo + name: demo-workflow + version: 0.0.1 + title: Demo Workflow +do: + - demo-workflow: + do: + - greet: + set: + message: Hello, World! + metadata: + __zigflow_id: 15b9ba59-d039-46cd-9d88-0b4adeae4d3d + - pause: + wait: + seconds: 5 + metadata: + __zigflow_id: 76e94b56-cdef-4d2c-8e34-9ef4272c2022 + - route: + switch: + - fast-path: + then: route-fast-path + when: ${ $input.fast == true } + metadata: + __zigflow_id: f24ed99f-e0f0-4779-8ab6-b013f2589889 + - default: + then: route-default + metadata: + __zigflow_id: 895e4b0a-ff1e-4ccd-9295-e6c7b763958c + metadata: + __zigflow_id: c4dd198a-b394-4a7e-81c1-764c87ae509b + - parallel-work: + fork: + compete: false + branches: + - branch-a: + do: [] + metadata: + __zigflow_id: 8fd21a81-26c0-4023-87c3-a284ee857f4c + - branch-b: + do: [] + metadata: + __zigflow_id: 742eca7b-6f91-4200-901f-31b76a74395b + metadata: + __zigflow_id: a28d11d1-6e35-453f-b2af-64811c0ff650 + - safe-call: + try: [] + catch: + do: [] + metadata: + __zigflow_id: 7e649e6b-50f6-4e93-a501-7f8ffaf4d6aa + - process-items: + for: + in: ${ $input.items } + each: item + at: index + do: + - wait: + wait: + seconds: 30 + metadata: + __zigflow_id: 088b542c-8d4c-43b5-8557-97d541e86343 + metadata: + __zigflow_id: e4879bd8-ed9e-4955-bff1-33490e86e5f7 + - wait: + wait: + seconds: 30 + metadata: + __zigflow_id: 1b9eeddd-d06c-4788-b855-22a0faf52523 + - route-fast-path: + do: [] + - route-default: + do: []