diff --git a/.claude/commands/documentar.md b/.claude/commands/documentar.md index 1e9cf57..1807d16 100644 --- a/.claude/commands/documentar.md +++ b/.claude/commands/documentar.md @@ -41,6 +41,8 @@ markdown_extensions: - tables - toc: permalink: true + - attr_list + - md_in_html extra_javascript: - https://unpkg.com/mermaid@10/dist/mermaid.min.js @@ -108,6 +110,68 @@ docs/ └── arquitetura.md ← diagrama Mermaid + descrição de componentes ``` +## Screenshots do frontend + +A documentação **deve incluir prints reais da interface**. Siga este processo: + +### 1. Verificar se o frontend está rodando + +Antes de tirar screenshots, verifique se o frontend já está acessível em `http://localhost:5173`. Use a ferramenta `browser_navigate` para navegar até a URL e `browser_take_screenshot` para capturar. + +Se o frontend não estiver rodando, inicie com: + +```bash +docker compose up frontend -d +``` + +Aguarde alguns segundos e verifique novamente. + +### 2. Screenshots a capturar + +Salve todos os screenshots em `docs/assets/screenshots/`. Crie a pasta se não existir. Use os nomes de arquivo abaixo (exatos, sem espaços): + +| Arquivo | O que capturar | +|----------------------------------|--------------------------------------------------------------------------------| +| `visao-geral.png` | A interface completa com painel lateral + canvas (domínio ecommerce carregado) | +| `modal-ia.png` | O modal de geração com IA aberto, com provider e prompt preenchidos | +| `diagrama-relacional.png` | O canvas com um diagrama relacional com pelo menos 3 tabelas conectadas | +| `catalogo-faker.png` | O painel do catálogo Faker aberto | +| `yaml-exportado.png` | A aba de YAML exportado com código visível | +| `run-generator.png` | O modal "Run Generator" / painel de execução aberto | + +### 3. Como tirar os screenshots + +Use a sequência de ferramentas MCP de browser: + +1. `browser_navigate` → `http://localhost:5173` +2. `browser_take_screenshot` → salva a visão geral +3. Para abrir modais: use `browser_click` no botão correspondente, depois `browser_take_screenshot` +4. Para carregar um domínio: use `browser_select_option` ou `browser_click` no seletor de domínio + +Após cada screenshot, salve o arquivo em `docs/assets/screenshots/.png` usando a ferramenta `Write` com o conteúdo binário retornado, **ou** use `browser_take_screenshot` com o parâmetro de caminho de saída se disponível. + +### 4. Referenciar nas páginas + +Após salvar os screenshots, referencie-os nas páginas correspondentes do MkDocs: + +- `docs/frontend/visao-geral.md` → `![Visão geral da interface](../assets/screenshots/visao-geral.png)` +- `docs/frontend/ia.md` → `![Modal de geração com IA](../assets/screenshots/modal-ia.png)` +- `docs/frontend/diagrama.md` → `![Diagrama relacional](../assets/screenshots/diagrama-relacional.png)` +- `docs/frontend/faker.md` → `![Catálogo Faker](../assets/screenshots/catalogo-faker.png)` +- `docs/frontend/yaml.md` → `![YAML exportado](../assets/screenshots/yaml-exportado.png)` + +Posicione cada imagem logo após o primeiro parágrafo introdutório da página, antes do detalhamento. + +### 5. Se o frontend não puder ser iniciado + +Se após tentar subir com Docker o frontend ainda não estiver acessível, registre no chat: + +> "Frontend indisponível para screenshots. As páginas de interface foram documentadas sem capturas de tela. Execute `docker compose up frontend` e rode `/documentar` novamente para adicionar os prints." + +Não bloqueie a geração do restante da documentação por causa dos screenshots — eles são adicionais, não bloqueantes. + +--- + ## O que ler antes de escrever Leia os arquivos abaixo para entender o estado atual do projeto antes de escrever qualquer coisa: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8df1797..c93fde2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches-ignore: [] # runs on all branches including main + branches-ignore: ["main"] pull_request: branches: ["main"] @@ -10,7 +10,6 @@ jobs: lint: name: Lint & Format runs-on: ubuntu-latest - if: github.ref_name != 'main' steps: - uses: actions/checkout@v4 @@ -26,7 +25,6 @@ jobs: name: Tests runs-on: ubuntu-latest needs: lint - if: github.ref_name != 'main' steps: - uses: actions/checkout@v4 @@ -52,11 +50,31 @@ jobs: files: coverage.xml fail_ci_if_error: false + frontend-test: + name: Frontend Tests + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: src/dataforge/frontend/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: src/dataforge/frontend + + - name: Run tests + run: npm test + working-directory: src/dataforge/frontend + build: name: Build Package runs-on: ubuntu-latest - needs: test - if: github.ref_name != 'main' + needs: [test, frontend-test] steps: - uses: actions/checkout@v4 @@ -76,8 +94,8 @@ jobs: open-pr: name: Open PR to main runs-on: ubuntu-latest - needs: [lint, test] - if: github.event_name == 'push' && github.ref_name != 'main' + needs: [lint, test, frontend-test] + if: github.event_name == 'push' permissions: pull-requests: write contents: read @@ -104,68 +122,3 @@ jobs: --title "feat: merge ${BRANCH} into main (v${VERSION})" \ --body-file /tmp/pr_body.md fi - - docs: - name: Deploy Docs - runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref_name == 'main' - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install MkDocs - run: pip install mkdocs mkdocs-material - - - name: Deploy to GitHub Pages - run: mkdocs gh-deploy --force - - release: - name: Release - runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref_name == 'main' - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Read version from pyproject.toml - id: version - run: | - VERSION=$(grep -m1 '^version' pyproject.toml | sed 's/.*"\(.*\)"/\1/') - echo "value=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" - - - name: Check if tag already exists - id: tag_check - run: | - if git ls-remote --tags origin "refs/tags/${{ steps.version.outputs.tag }}" | grep -q .; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - - name: Create git tag - if: steps.tag_check.outputs.exists == 'false' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "${{ steps.version.outputs.tag }}" - git push origin "${{ steps.version.outputs.tag }}" - - - name: Create GitHub Release - if: steps.tag_check.outputs.exists == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create "${{ steps.version.outputs.tag }}" \ - --title "Dataforge ${{ steps.version.outputs.tag }}" \ - --generate-notes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..17dadd2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +name: Release + +on: + push: + branches: ["main"] + +jobs: + docs: + name: Deploy Docs + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install MkDocs + run: pip install mkdocs mkdocs-material + + - name: Deploy to GitHub Pages + run: mkdocs gh-deploy --force + + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Read version from pyproject.toml + id: version + run: | + VERSION=$(grep -m1 '^version' pyproject.toml | sed 's/.*"\(.*\)"/\1/') + echo "value=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + + - name: Check if tag already exists + id: tag_check + run: | + if git ls-remote --tags origin "refs/tags/${{ steps.version.outputs.tag }}" | grep -q .; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create git tag + if: steps.tag_check.outputs.exists == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${{ steps.version.outputs.tag }}" + git push origin "${{ steps.version.outputs.tag }}" + + - name: Create GitHub Release + if: steps.tag_check.outputs.exists == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ steps.version.outputs.tag }}" \ + --title "Dataforge ${{ steps.version.outputs.tag }}" \ + --generate-notes diff --git a/.gitignore b/.gitignore index 6ea852e..000c12d 100644 --- a/.gitignore +++ b/.gitignore @@ -195,6 +195,7 @@ pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode # Dataforge specific output/ +data/ credentials/* !credentials/*.example !credentials/*.example.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37db7d2..5e4bc09 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,3 +14,10 @@ repos: language: system pass_filenames: false always_run: true + + - id: frontend-test + name: vitest (frontend) + entry: bash -c 'cd src/dataforge/frontend && npm test' + language: system + pass_filenames: false + files: ^src/dataforge/frontend/src/.*\.(ts|tsx)$ diff --git a/docker-compose.yml b/docker-compose.yml index 60f9e9b..0325400 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,8 @@ services: # Volume nomeado protege o node_modules Linux do build # (impede que o mount acima sobrescreva com binários Windows do host) - frontend_node_modules:/app/src/dataforge/frontend/node_modules + # Dados persistidos: usuários, env-keys, JWT secret + - ./data:/app/data environment: - PYTHONPATH=/app/src restart: unless-stopped diff --git a/docs/assets/screenshots/catalogo-faker.png b/docs/assets/screenshots/catalogo-faker.png new file mode 100644 index 0000000..587caed Binary files /dev/null and b/docs/assets/screenshots/catalogo-faker.png differ diff --git a/docs/assets/screenshots/diagrama-relacional.png b/docs/assets/screenshots/diagrama-relacional.png new file mode 100644 index 0000000..4881024 Binary files /dev/null and b/docs/assets/screenshots/diagrama-relacional.png differ diff --git a/docs/assets/screenshots/modal-ia.png b/docs/assets/screenshots/modal-ia.png new file mode 100644 index 0000000..13e37ca Binary files /dev/null and b/docs/assets/screenshots/modal-ia.png differ diff --git a/docs/assets/screenshots/run-generator.png b/docs/assets/screenshots/run-generator.png new file mode 100644 index 0000000..4dddffd Binary files /dev/null and b/docs/assets/screenshots/run-generator.png differ diff --git a/docs/assets/screenshots/visao-geral.png b/docs/assets/screenshots/visao-geral.png new file mode 100644 index 0000000..2c79d68 Binary files /dev/null and b/docs/assets/screenshots/visao-geral.png differ diff --git a/docs/assets/screenshots/yaml-exportado.png b/docs/assets/screenshots/yaml-exportado.png new file mode 100644 index 0000000..b871fef Binary files /dev/null and b/docs/assets/screenshots/yaml-exportado.png differ diff --git a/docs/avancado/arquitetura.md b/docs/avancado/arquitetura.md index 981a902..b564d64 100644 --- a/docs/avancado/arquitetura.md +++ b/docs/avancado/arquitetura.md @@ -14,10 +14,13 @@ flowchart LR UI --> SW end - subgraph Server["Servidor Vite / Backend Python"] - API_SCHEMAS[GET /api/schemas\nGET /api/schemas/:nome\nPUT /api/schemas/:nome\nDELETE /api/schemas/:nome] + subgraph Server["Servidor Vite (Node.js)"] + API_AUTH[POST /api/auth/register\nPOST /api/auth/login] + API_SCHEMAS[GET /api/schemas\nPOST /api/save-schema] API_AI[POST /api/ai-generate\nPOST /api/ai-models] API_DB[POST /api/test-db-connection] + API_CLI[POST /api/run-cli\nPOST /api/stop-cli\nGET /api/run-history] + API_SCHED[GET /api/schedules\nPOST /api/schedules] end subgraph Core["Core Python"] @@ -45,9 +48,12 @@ flowchart LR OLLAMA[Ollama local] end + UI -- HTTP --> API_AUTH UI -- HTTP --> API_SCHEMAS UI -- HTTP --> API_AI UI -- HTTP --> API_DB + UI -- HTTP --> API_CLI + UI -- HTTP --> API_SCHED API_AI --> ANTHROPIC API_AI --> OPENAI API_AI --> GOOGLE @@ -81,17 +87,27 @@ flowchart LR ### Servidor (vite.config.ts) -O servidor de desenvolvimento do Vite expõe endpoints de API via proxy ou plugin. Os endpoints disponíveis são: +O servidor de desenvolvimento do Vite expõe endpoints de API via plugin. Os endpoints disponíveis são: | Endpoint | Método | Descrição | |----------|--------|-----------| +| `/api/auth/register` | POST | Registra novo usuário | +| `/api/auth/login` | POST | Autentica usuário e retorna JWT | +| `/api/auth/me` | GET | Retorna dados do usuário autenticado | | `/api/schemas` | GET | Lista schemas disponíveis | -| `/api/schemas/:nome` | GET | Retorna o YAML de um schema | -| `/api/schemas/:nome` | PUT | Salva um schema no servidor | -| `/api/schemas/:nome` | DELETE | Remove um schema do servidor | +| `/api/save-schema` | POST | Salva schema no servidor | | `/api/ai-generate` | POST | Gera schema via IA | | `/api/ai-models` | POST | Lista modelos disponíveis para o provider | | `/api/test-db-connection` | POST | Testa uma connection string SQL | +| `/api/run-cli` | POST | Executa o CLI com os parâmetros fornecidos | +| `/api/stop-cli` | POST | Para a execução do CLI em andamento | +| `/api/run-history` | GET | Lista o histórico de execuções | +| `/api/schedules` | GET | Lista execuções agendadas | +| `/api/schedules` | POST | Cria uma nova execução agendada | +| `/api/credential-profiles` | GET/POST | Gerencia perfis de credenciais cloud | +| `/api/profile/env-keys` | GET/POST/DELETE | Gerencia chaves de API salvas por usuário | +| `/api/capabilities` | GET | Retorna capacidades disponíveis no servidor | +| `/api/browse-folder` | GET | Navega pelo sistema de arquivos do servidor | ### Core Python diff --git a/docs/cli/referencia.md b/docs/cli/referencia.md index b77b463..3e08972 100644 --- a/docs/cli/referencia.md +++ b/docs/cli/referencia.md @@ -35,9 +35,11 @@ Gera datasets e opcionalmente escreve em arquivos, faz upload para nuvem ou carr | `--db-url` | | — | string | Connection string SQLAlchemy para carga SQL | | `--if-exists` | | `replace` | choice | O que fazer se a tabela SQL já existir: `replace`, `append` ou `fail` | | `--db-schema` | | — | string | Schema do banco de destino | +| `--partition-date-granularity` | | — | string | Trunca valores de data nas partições: `granularidade` ou `tabela:granularidade`. Opções: `year` (→ YYYY) ou `month` (→ YYYY-MM) (repetível) | | `--recurrence` | `-R` | — | float | Intervalo em segundos entre batches (modo contínuo) | | `--count` | | `0` | int | Número de batches no modo recorrente (`0` = infinito) | | `--increment` | | — | string | Desloca valores de coluna a cada batch: `tabela:coluna:passo[:unidade]` (repetível) | +| `--workers` | | `16` | int | Máximo de threads paralelas para escrita particionada | ### Unidades para `--increment` diff --git a/docs/frontend/diagrama.md b/docs/frontend/diagrama.md index b7506e9..95b976c 100644 --- a/docs/frontend/diagrama.md +++ b/docs/frontend/diagrama.md @@ -2,6 +2,8 @@ O canvas central da interface exibe as tabelas do schema como nós interativos conectados por setas que representam as chaves estrangeiras. +![Diagrama relacional](../assets/screenshots/diagrama-relacional.png) + ## Tecnologia O diagrama é construído com **ReactFlow**. Cada tabela é um nó do tipo `tableNode` (componente customizado) e cada FK é uma aresta animada. diff --git a/docs/frontend/faker.md b/docs/frontend/faker.md index 57720a7..18e6147 100644 --- a/docs/frontend/faker.md +++ b/docs/frontend/faker.md @@ -2,6 +2,8 @@ O **Faker Browser** é um painel de busca visual com todos os métodos do Faker disponíveis na interface. Ele permite explorar e aplicar provedores de dados diretamente às colunas do schema. +![Catálogo Faker](../assets/screenshots/catalogo-faker.png) + ## Acessando Clique no botão **Faker Browser** na barra de ações. O painel lateral é aberto com campo de busca e listagem por categoria. diff --git a/docs/frontend/ia.md b/docs/frontend/ia.md index 7dd3042..01c8c89 100644 --- a/docs/frontend/ia.md +++ b/docs/frontend/ia.md @@ -2,6 +2,8 @@ O recurso **AI Generate** permite descrever um domínio de negócio em linguagem natural e gerar automaticamente o schema YAML com tabelas, colunas, tipos de dados e relações. +![Modal de geração com IA](../assets/screenshots/modal-ia.png) + ## Fluxo completo ```mermaid diff --git a/docs/frontend/visao-geral.md b/docs/frontend/visao-geral.md index dcc4074..a84d057 100644 --- a/docs/frontend/visao-geral.md +++ b/docs/frontend/visao-geral.md @@ -2,6 +2,14 @@ A interface visual roda em `http://localhost:5173` e é a forma principal de uso do Dataforge. Ela permite criar schemas, configurar a geração e executar o CLI sem sair do navegador. +![Visão geral da interface](../assets/screenshots/visao-geral.png) + +## Autenticação + +O acesso à interface requer login. Na primeira vez, crie uma conta pela aba **Create Account** na tela de login. O servidor registra as credenciais localmente em `users.json` (dentro do container/projeto). Após autenticar, um JWT é salvo no `localStorage` com a chave `dataforge_auth` e enviado em todas as chamadas de API. + +Para sair, clique no ícone de usuário no canto superior direito e selecione **Sign Out**. + ## Tecnologias - **React** com TypeScript diff --git a/docs/frontend/yaml.md b/docs/frontend/yaml.md index 1dd00c3..80eea1c 100644 --- a/docs/frontend/yaml.md +++ b/docs/frontend/yaml.md @@ -2,6 +2,8 @@ A interface permite exportar o schema atual como YAML e importar um arquivo YAML existente para o canvas. +![YAML exportado](../assets/screenshots/yaml-exportado.png) + ## Exportar YAML (Preview) O botão **Preview YAML** gera o YAML do schema atual e exibe no painel lateral. diff --git a/docs/saida/particionamento.md b/docs/saida/particionamento.md index 9da1e19..dfc1dff 100644 --- a/docs/saida/particionamento.md +++ b/docs/saida/particionamento.md @@ -77,5 +77,50 @@ O layout Hive-style é reconhecido diretamente por: - **DuckDB** — `read_parquet("output/ecommerce/orders/**/*.parquet", hive_partitioning=true)` - **dbt** — uso como source com partições +## Granularidade de data nas partições + +Quando a coluna de partição contém datas, os valores brutos como `2024-03-15` geram muitas subpastas. Use `--partition-date-granularity` para truncar esses valores: + +| Granularidade | Formato gerado | Exemplo de pasta | +|---------------|----------------|------------------| +| `year` | `YYYY` | `created_at=2024` | +| `month` | `YYYY-MM` | `created_at=2024-03` | + +```bash +# Particionar orders por mês +docker compose run --rm cli generate -d ecommerce -f parquet \ + --partition-by "orders:created_at" \ + --partition-date-granularity "orders:month" + +# Aplicar granularidade anual a todas as tabelas +docker compose run --rm cli generate -d ecommerce -f parquet \ + --partition-by created_at \ + --partition-date-granularity year +``` + +Layout de saída com `--partition-date-granularity month`: + +``` +output/ +└── ecommerce/ + └── orders/ + ├── created_at=2023-01/ + │ └── orders.parquet + ├── created_at=2023-02/ + │ └── orders.parquet + └── created_at=2024-12/ + └── orders.parquet +``` + +## Paralelismo na escrita + +Por padrão, a escrita de partições usa até 16 threads em paralelo. Ajuste com `--workers`: + +```bash +docker compose run --rm cli generate -d ecommerce -f parquet \ + --partition-by "orders:status" \ + --workers 4 +``` + !!! tip "Colunas de data" Particionar por uma coluna de data (`created_at`, `transaction_date`) é o padrão mais comum para datasets temporais. Combine com o modo recorrente e `--increment` para simular pipelines incrementais. diff --git a/pyproject.toml b/pyproject.toml index 22ada9b..8adb3dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dataforge" -version = "0.1.8" +version = "0.1.9" description = "Synthetic relational dataset generator for data engineering studies" authors = [ {name = "Carlos Oliveira", email = "papodedados@gmail.com"} diff --git a/src/dataforge/cli.py b/src/dataforge/cli.py index 03f5f9b..4a2be5a 100644 --- a/src/dataforge/cli.py +++ b/src/dataforge/cli.py @@ -358,7 +358,8 @@ def _append_csv( dest_dir = out_path / dataset_name / name / f"{partition_by}={safe_val}" dest_dir.mkdir(parents=True, exist_ok=True) path = dest_dir / f"{name}.csv" - group.to_csv(path, mode="a", index=False, header=not path.exists()) + write_header = not path.exists() or path.stat().st_size == 0 + group.to_csv(path, mode="a", index=False, header=write_header) click.echo(f" [csv/append] {path} (+{len(group)} rows)") written.append( (path, f"{dataset_name}/{name}/{partition_by}={safe_val}/{path.name}") @@ -367,7 +368,8 @@ def _append_csv( dest_dir = out_path / dataset_name / name dest_dir.mkdir(parents=True, exist_ok=True) path = dest_dir / f"{name}.csv" - df.to_csv(path, mode="a", index=False, header=not path.exists()) + write_header = not path.exists() or path.stat().st_size == 0 + df.to_csv(path, mode="a", index=False, header=write_header) click.echo(f" [csv/append] {path} (+{len(df)} rows)") written.append((path, f"{dataset_name}/{name}/{path.name}")) return written diff --git a/src/dataforge/frontend/package-lock.json b/src/dataforge/frontend/package-lock.json index 41932a4..805abea 100644 --- a/src/dataforge/frontend/package-lock.json +++ b/src/dataforge/frontend/package-lock.json @@ -9,28 +9,83 @@ "version": "0.0.0", "dependencies": { "dagre": "^0.8.5", + "i18next": "^26.0.4", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.8.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-i18next": "^17.0.2", "reactflow": "^11.11.4", "yaml": "^2.8.3" }, "devDependencies": { "@eslint/js": "^9.39.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/dagre": "^0.7.54", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^4.1.4", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "jsdom": "^29.0.2", "typescript": "~6.0.2", "typescript-eslint": "^8.58.0", - "vite": "^8.0.4" + "vite": "^8.0.4", + "vitest": "^4.1.4" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.10.tgz", + "integrity": "sha512-02OhhkKtgNRuicQ/nF3TRnGsxL9wp0r3Y7VlKWyOHHGmGyvXv03y+PnymU8FKFJMTjIr1Bk8U2g1HWSLrpAHww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.9.tgz", + "integrity": "sha512-r3ElRr7y8ucyN2KdICwGsmj19RoN13CLCa/pvGydghWK6ZzeKQ+TcDjVdtEZz2ElpndM5jXw//B9CEee0mWnVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -223,6 +278,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -271,6 +335,169 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/core": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", @@ -462,6 +689,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -959,6 +1204,103 @@ "dev": true, "license": "MIT" }, + "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/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -970,6 +1312,25 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -1230,6 +1591,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1601,69 +1969,263 @@ } } }, - "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==", + "node_modules/@vitest/coverage-v8": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.4.tgz", + "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.4", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.4", + "vitest": "4.1.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "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==", + "node_modules/@vitest/expect": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "node_modules/@vitest/mocker": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@vitest/spy": "4.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "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==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/argparse": { - "version": "2.0.1", + "node_modules/@vitest/runner": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "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": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1684,6 +2246,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", @@ -1760,6 +2332,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1832,6 +2414,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1954,6 +2557,20 @@ "lodash": "^4.17.15" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1972,6 +2589,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1979,6 +2603,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1989,6 +2623,14 @@ "node": ">=8" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/electron-to-chromium": { "version": "1.5.334", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", @@ -1996,6 +2638,26 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2193,6 +2855,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2203,6 +2875,16 @@ "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", @@ -2380,6 +3062,75 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.0.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.4.tgz", + "integrity": "sha512-gXF7U9bfioXPLv7mw8Qt2nfO7vij5MyINvPgVv99pX3fL1Y01pw2mKBFrlYpRxRCl2wz3ISenj6VsMJT2isfuA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2417,6 +3168,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2440,6 +3201,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2447,6 +3215,45 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2467,7 +3274,58 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { + "node_modules/jsdom": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", + "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.5", + "@asamuzakjp/dom-selector": "^7.0.6", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", @@ -2847,6 +3705,85 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/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": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -2900,6 +3837,17 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "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", @@ -2963,6 +3911,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2983,6 +3944,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3042,6 +4010,36 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3073,6 +4071,41 @@ "react": "^19.2.5" } }, + "node_modules/react-i18next": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.2.tgz", + "integrity": "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/reactflow": { "version": "11.11.4", "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", @@ -3091,6 +4124,30 @@ "react-dom": ">=17" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3142,6 +4199,19 @@ "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3181,6 +4251,13 @@ "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/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3191,6 +4268,33 @@ "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": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3217,6 +4321,30 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "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": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -3234,6 +4362,62 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", + "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.28" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", + "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -3272,7 +4456,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3306,6 +4490,16 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -3441,6 +4635,153 @@ } } }, + "node_modules/vitest": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3457,6 +4798,23 @@ "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", @@ -3467,6 +4825,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/src/dataforge/frontend/package.json b/src/dataforge/frontend/package.json index 0538caf..5f7759d 100644 --- a/src/dataforge/frontend/package.json +++ b/src/dataforge/frontend/package.json @@ -8,29 +8,41 @@ "typecheck": "tsc --noEmit", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "dagre": "^0.8.5", + "i18next": "^26.0.4", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.8.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-i18next": "^17.0.2", "reactflow": "^11.11.4", "yaml": "^2.8.3" }, "devDependencies": { "@eslint/js": "^9.39.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/dagre": "^0.7.54", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^4.1.4", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "jsdom": "^29.0.2", "typescript": "~6.0.2", "typescript-eslint": "^8.58.0", - "vite": "^8.0.4" + "vite": "^8.0.4", + "vitest": "^4.1.4" } } diff --git a/src/dataforge/frontend/src/App.tsx b/src/dataforge/frontend/src/App.tsx index 2244996..8f7bc86 100644 --- a/src/dataforge/frontend/src/App.tsx +++ b/src/dataforge/frontend/src/App.tsx @@ -1,9 +1,196 @@ import React, { useState, useRef, useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import dagre from 'dagre'; import ReactFlow, { Background, Controls, ConnectionLineType, useNodesState, useEdgesState, MarkerType } from 'reactflow'; import type { Edge, Node } from 'reactflow'; import 'reactflow/dist/style.css'; // ReactFlow v11 has no non-dist CSS export — necessary exception -import { Plus, Download, FileJson, Trash2, Key, Link as LinkIcon, X, Network, Play, BookOpen, Search, Sparkles } from 'lucide-react'; +import { Plus, Download, FileJson, Trash2, Key, Link as LinkIcon, X, Network, Play, BookOpen, Search, Sparkles, Clock, History, CheckCircle, XCircle, Loader, ChevronDown, ChevronUp, User, LogOut, Eye, EyeOff, Pencil, Check } from 'lucide-react'; + +// ── EnvKeyPicker ────────────────────────────────────────────────────────────── +// Renders a select of available profile env-keys. +// value = key NAME (e.g. "AWS_ACCESS_KEY_ID"), not the secret value. +interface EnvKeyPickerProps { + label: string; + value: string; + onChange: (keyName: string) => void; + envKeys: Record; + onOpenProfile: () => void; + hint?: string; // expected key name shown as placeholder hint +} + +function EnvKeyPicker({ label, value, onChange, envKeys, onOpenProfile, hint }: EnvKeyPickerProps) { + const { t } = useTranslation(); + const keys = Object.keys(envKeys); + const resolved = value ? envKeys[value] : undefined; + const missing = value && !resolved; + + return ( +
+
+ + +
+ + {keys.length === 0 ? ( +
+ + {t('profile.noKeysYet')}{' '} + +
+ ) : ( + <> + + {value && ( +
+ {resolved + ? <> {t('profile.resolved')} · {resolved.length > 4 ? resolved.slice(0, 4) + '••••' : '••••'} + : <> {t('profile.keyNotFound', { value })}} +
+ )} + + )} +
+ ); +} + +// ── Auth ────────────────────────────────────────────────────────────────────── +const AUTH_STORAGE_KEY = 'dataforge_auth'; + +function loadAuth(): { token: string; username: string } | null { + try { return JSON.parse(localStorage.getItem(AUTH_STORAGE_KEY) || 'null'); } catch { return null; } +} +function saveAuth(token: string, username: string) { + localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token, username })); +} +function clearAuth() { + localStorage.removeItem(AUTH_STORAGE_KEY); +} + +function authHeaders(token: string) { + return { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }; +} + +// ── LoginScreen ─────────────────────────────────────────────────────────────── +function LoginScreen({ onAuth }: { onAuth: (token: string, username: string) => void }) { + const { t } = useTranslation(); + const [mode, setMode] = useState<'login' | 'register'>('login'); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPw, setShowPw] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const submit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + try { + const endpoint = mode === 'login' ? '/api/auth/login' : '/api/auth/register'; + const res = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: username.trim(), password }), + }); + const data = await res.json(); + if (!res.ok) { setError(data.error || t('auth.somethingWentWrong')); return; } + saveAuth(data.token, data.username); + onAuth(data.token, data.username); + } catch (e: any) { + setError(e.message || t('auth.networkError')); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Logo */} +
+ Dataforge +
+ Dataforge +
+
{t('auth.subtitle')}
+
+ + {/* Tabs */} +
+ {(['login', 'register'] as const).map(m => ( + + ))} +
+ +
+
+ + setUsername(e.target.value)} + placeholder={t('auth.usernamePlaceholder')} + style={{ width: '100%', padding: '0.6rem 0.75rem' }} + disabled={loading} + /> +
+
+ +
+ setPassword(e.target.value)} + placeholder={t('auth.passwordPlaceholder')} + style={{ width: '100%', padding: '0.6rem 2.5rem 0.6rem 0.75rem' }} + disabled={loading} + /> + +
+
+ + {error && ( +
+ {error} +
+ )} + + +
+
+
+ ); +} const FAKER_CATALOG: { category: string; color: string; methods: { name: string; example: string }[] }[] = [ { category: 'Person', color: '#60a5fa', methods: [ @@ -166,7 +353,73 @@ function isDateColumn(col: Column): boolean { } -export default function App() { +function AppMain({ auth, onLogout }: { auth: { token: string; username: string }; onLogout: () => void }) { + const { t, i18n } = useTranslation(); + // ── Profile / env-keys state ─────────────────────────────────────────────── + const [showProfilePanel, setShowProfilePanel] = useState(false); + const [envKeys, setEnvKeys] = useState>({}); + const [envPreviewVisible, setEnvPreviewVisible] = useState(false); + const [envKeyForm, setEnvKeyForm] = useState({ key: '', value: '' }); + const [envKeyError, setEnvKeyError] = useState(''); + const [envKeyLoading, setEnvKeyLoading] = useState(false); + const [showEnvValue, setShowEnvValue] = useState>({}); + const [editingEnvKey, setEditingEnvKey] = useState(null); + const [editingEnvValue, setEditingEnvValue] = useState(''); + + const handleSaveEditEnvKey = async () => { + if (!editingEnvKey) return; + setEnvKeyLoading(true); + try { + const res = await fetch('/api/profile/env-keys', { + method: 'POST', + headers: authHeaders(auth.token), + body: JSON.stringify({ key: editingEnvKey, value: editingEnvValue }), + }); + if (res.ok) { setEditingEnvKey(null); setEditingEnvValue(''); await fetchEnvKeys(); } + } finally { + setEnvKeyLoading(false); + } + }; + + const fetchEnvKeys = useCallback(async () => { + try { + const res = await fetch('/api/profile/env-keys', { headers: { Authorization: `Bearer ${auth.token}` } }); + if (res.ok) setEnvKeys(await res.json()); + } catch { /* ignore */ } + }, [auth.token]); + + useEffect(() => { fetchEnvKeys(); }, [fetchEnvKeys]); + + const handleAddEnvKey = async () => { + setEnvKeyError(''); + const k = envKeyForm.key.trim(); + const v = envKeyForm.value; + if (!k) { setEnvKeyError('Key name is required.'); return; } + setEnvKeyLoading(true); + try { + const res = await fetch('/api/profile/env-keys', { + method: 'POST', + headers: authHeaders(auth.token), + body: JSON.stringify({ key: k, value: v }), + }); + const data = await res.json(); + if (!res.ok) { setEnvKeyError(data.error || 'Failed to save key.'); return; } + setEnvKeyForm({ key: '', value: '' }); + await fetchEnvKeys(); + } catch (e: any) { + setEnvKeyError(e.message || String(e)); + } finally { + setEnvKeyLoading(false); + } + }; + + const handleDeleteEnvKey = async (keyName: string) => { + await fetch(`/api/profile/env-keys/${encodeURIComponent(keyName)}`, { + method: 'DELETE', headers: { Authorization: `Bearer ${auth.token}` }, + }); + await fetchEnvKeys(); + }; + const [domain, setDomain] = useState("custom"); const [tables, setTables] = useState([]); const [generatedYaml, setGeneratedYaml] = useState(''); @@ -305,7 +558,7 @@ export default function App() { position: { x: window.innerWidth / 2 - 100, y: window.innerHeight / 2 - 100 } } ]); - setSelectedTableId(newId); + setSelectedTableId(tableId); }; const generateSchema = () => { @@ -350,8 +603,8 @@ export default function App() { source: sourceTable.id, target: targetTable.id, animated: true, - style: { stroke: '#ec4899', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' } + style: { stroke: 'var(--accent)', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#fb923c' } }); } } @@ -504,11 +757,7 @@ export default function App() { { key: 'ollama', label: 'Ollama', color: '#94a3b8', keyPlaceholder: '', modelPlaceholder: 'e.g. llama3.2' }, ] as const; - const AI_DEFAULT_PROMPT = `E-commerce with 4 tables: -- customers: full name, email, phone, city, country, registration date -- products: name, category (Electronics/Clothing/Books/Home/Sports), price (10–2000), stock quantity -- orders: linked to customer, order date (last 2 years), status (pending/processing/shipped/delivered/cancelled), total amount -- order_items: linked to order and product, quantity (1–10), unit price`; + const AI_DEFAULT_PROMPT = t('ai.defaultPrompt'); const [aiModal, setAiModal] = useState(false); const [aiProvider, setAiProvider] = useState(() => loadAiConfig().provider || 'anthropic'); @@ -522,7 +771,8 @@ export default function App() { const [aiError, setAiError] = useState(''); const currentProviderMeta = AI_PROVIDERS.find(p => p.key === aiProvider) ?? AI_PROVIDERS[0]; - const currentApiKey = aiApiKeys[aiProvider] ?? ''; + const currentApiKeyRef = aiApiKeys[aiProvider] ?? ''; // env key name selected + const currentApiKey = envKeys[currentApiKeyRef] ?? ''; // resolved value const currentModel = aiModels[aiProvider] ?? ''; const availableModels = aiAvailableModels[aiProvider] ?? []; @@ -551,19 +801,18 @@ export default function App() { const handleAiGenerate = async () => { setAiError(''); const isOllama = aiProvider === 'ollama'; - if (!currentApiKey.trim() && !isOllama) { setAiError('API key is required.'); return; } - if (!aiPrompt.trim()) { setAiError('Describe the domain you want to generate.'); return; } + if (!currentApiKey.trim() && !isOllama) { setAiError(t('ai.apiKeyRequired')); return; } + if (!aiPrompt.trim()) { setAiError(t('ai.describeTheDomain')); return; } setAiLoading(true); try { - const savedKeys = { ...aiApiKeys }; + const savedKeys = { ...aiApiKeys }; // stores key NAMES const savedModels = { ...aiModels }; - if (currentApiKey.trim()) savedKeys[aiProvider] = currentApiKey.trim(); if (currentModel.trim()) savedModels[aiProvider] = currentModel.trim(); localStorage.setItem(AI_KEY_STORAGE, JSON.stringify({ provider: aiProvider, apiKeys: savedKeys, models: savedModels })); const res = await fetch('/api/ai-generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ provider: aiProvider, apiKey: currentApiKey.trim(), model: currentModel.trim() || undefined, prompt: aiPrompt }), + body: JSON.stringify({ provider: aiProvider, apiKey: currentApiKey.trim(), model: currentModel.trim() || undefined, prompt: `${t('ai.langInstruction')}\n\n${aiPrompt}` }), }); const data = await res.json(); if (!res.ok || data.error) { @@ -596,8 +845,8 @@ export default function App() { const handleDeleteDomain = () => { showConfirm( - 'Delete schema', - `Remove "${domain}" permanently? This cannot be undone.`, + t('save.deleteSchema'), + t('save.deleteConfirm', { domain }), async () => { await fetch(`/api/schemas/${domain}`, { method: 'DELETE' }); window.location.reload(); @@ -620,7 +869,7 @@ export default function App() { body: JSON.stringify({ yamlStr, name }), }); const data = await res.json(); - if (!data.success) { setSaveError(data.error || 'Save failed.'); return; } + if (!data.success) { setSaveError(data.error || t('save.saveFailed')); return; } setSaveModal(false); setSaveName(''); window.location.reload(); @@ -629,6 +878,416 @@ export default function App() { } }; + // ── Schedule types ────────────────────────────────────────────────────────── + interface ScheduleDef { + id: string; + name: string; + cronExpression: string; + enabled: boolean; + config: Record; + createdAt: string; + } + interface RunRecord { + id: string; + scheduleId: string; + scheduleName: string; + triggeredBy: 'cron' | 'manual'; + startedAt: string; + finishedAt: string | null; + status: 'running' | 'success' | 'error'; + output: string; + exitCode: number | null; + } + + // ── Schedule state ────────────────────────────────────────────────────────── + const [showSchedulesPanel, setShowSchedulesPanel] = useState(false); + const [showHistoryPanel, setShowHistoryPanel] = useState(false); + const [schedulesLoadError, setSchedulesLoadError] = useState(''); + const [scheduleDefs, setScheduleDefs] = useState([]); + const [runHistory, setRunHistory] = useState([]); + const [historyOutput, setHistoryOutput] = useState(null); + const [showScheduleForm, setShowScheduleForm] = useState(false); + const [editingSchedule, setEditingSchedule] = useState(null); + + type SchedDest = 'local' | 'cloud' | 'database'; + + // ── Schedule builder mode ─────────────────────────────────────────────────── + const [schedBuilderMode, setSchedBuilderMode] = useState<'visual' | 'cron'>('visual'); + const [schedVisual, setSchedVisual] = useState({ minute: '0', hour: '9', day: '*', month: '*' }); + + const visualToCron = (v: typeof schedVisual) => + `${v.minute} ${v.hour} ${v.day} ${v.month} *`; + + const applyVisual = (next: Partial) => { + const merged = { ...schedVisual, ...next }; + setSchedVisual(merged); + setSchedForm(f => ({ ...f, cronExpression: visualToCron(merged) })); + }; + + const MONTHS = ['*','1','2','3','4','5','6','7','8','9','10','11','12']; + const MONTH_NAMES: Record = { '*':'Every month','1':'January','2':'February','3':'March','4':'April','5':'May','6':'June','7':'July','8':'August','9':'September','10':'October','11':'November','12':'December' }; + const DAYS_OF_MONTH = ['*', ...Array.from({length:31},(_,i)=>String(i+1))]; + + const [schedForm, setSchedForm] = useState<{ + name: string; + cronExpression: string; + destination: SchedDest; + formats: string[]; + outputDir: string; + rows: string; + jsonMode: string; + uploadTarget: string; + bucket: string; + prefix: string; + dbUrl: string; + ifExists: string; + dbSchema: string; + tablesToInclude: string[]; + cloudCreds: { gcsJson: string; s3AccessKey: string; s3SecretKey: string; s3Region: string; azureConnStr: string }; + dateAnchors: { table: string; column: string; offsetDays: string }[]; + }>({ + name: '', + cronExpression: '0 9 * * *', + destination: 'local', + formats: ['csv'], + outputDir: 'output', + rows: '', + jsonMode: 'flat', + uploadTarget: 'gcs', + bucket: '', + prefix: 'datasets/', + dbUrl: '', + ifExists: 'append', + dbSchema: '', + tablesToInclude: [], + cloudCreds: { gcsJson: '', s3AccessKey: '', s3SecretKey: '', s3Region: 'us-east-1', azureConnStr: '' }, + dateAnchors: [], + }); + const [schedSaving, setSchedSaving] = useState(false); + const [schedError, setSchedError] = useState(''); + + const fetchSchedules = async () => { + setSchedulesLoadError(''); + try { + const res = await fetch('/api/schedules', { headers: { Authorization: `Bearer ${auth?.token}` } }); + const data = await res.json(); + if (Array.isArray(data)) { + setScheduleDefs(data); + } else { + const msg = data?.error ?? `HTTP ${res.status}`; + setSchedulesLoadError(msg); + console.error('[schedules] unexpected response:', data); + } + } catch (e: any) { + setSchedulesLoadError(e?.message ?? 'Network error'); + console.error('[schedules] fetch error:', e); + } + }; + + const fetchHistory = async () => { + try { + const res = await fetch('/api/run-history?limit=100', { headers: { Authorization: `Bearer ${auth?.token}` } }); + const data = await res.json(); + if (Array.isArray(data)) setRunHistory(data); + else console.error('[run-history] unexpected response:', data); + } catch (e) { + console.error('[run-history] fetch error:', e); + } + }; + + const handleSaveSchedule = async () => { + setSchedError(''); + setSchedSaving(true); + try { + const yamlStr = SchemaWriter.generateYaml(domain, tables); + const config = { + yamlStr, + tables: schedForm.tablesToInclude.length > 0 ? schedForm.tablesToInclude : undefined, + rows: schedForm.rows !== '' ? parseInt(schedForm.rows) : undefined, + formats: schedForm.formats, + outputDir: schedForm.destination === 'local' ? schedForm.outputDir : undefined, + uploadTarget: schedForm.destination === 'cloud' ? schedForm.uploadTarget : undefined, + bucket: schedForm.bucket.trim() || undefined, + prefix: schedForm.prefix.trim() || undefined, + jsonMode: schedForm.jsonMode, + dbUrl: schedForm.destination === 'database' ? schedForm.dbUrl.trim() || undefined : undefined, + ifExists: schedForm.ifExists, + dbSchema: schedForm.dbSchema.trim() || undefined, + cloudCreds: schedForm.destination === 'cloud' ? schedForm.cloudCreds : undefined, + dateAnchors: schedForm.dateAnchors + .filter(a => a.table && a.column && a.offsetDays !== '') + .map(a => ({ table: a.table, column: a.column, offsetDays: parseInt(a.offsetDays) })), + }; + + const url = editingSchedule ? `/api/schedules/${editingSchedule.id}` : '/api/schedules'; + const method = editingSchedule ? 'PUT' : 'POST'; + const res = await fetch(url, { + method, + headers: authHeaders(auth.token), + body: JSON.stringify({ name: schedForm.name.trim(), cronExpression: schedForm.cronExpression.trim(), config }), + }); + const data = await res.json(); + if (!res.ok) { setSchedError(data.error || 'Save failed'); return; } + setShowScheduleForm(false); + setEditingSchedule(null); + fetchSchedules(); + } catch (e: any) { + setSchedError(e.message || String(e)); + } finally { + setSchedSaving(false); + } + }; + + const handleToggleSchedule = async (s: ScheduleDef) => { + await fetch(`/api/schedules/${s.id}`, { + method: 'PUT', + headers: authHeaders(auth.token), + body: JSON.stringify({ enabled: !s.enabled }), + }); + fetchSchedules(); + }; + + const handleDeleteSchedule = async (s: ScheduleDef) => { + showConfirm(t('schedule.deleteConfirmTitle', { name: s.name }), t('schedule.deleteConfirmMessage'), async () => { + await fetch(`/api/schedules/${s.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${auth.token}` } }); + fetchSchedules(); + }); + }; + + const handleRunNow = async (s: ScheduleDef) => { + await fetch(`/api/schedules/${s.id}/run`, { method: 'POST', headers: { Authorization: `Bearer ${auth.token}` } }); + setTimeout(() => fetchHistory(), 500); + }; + + type RegistryProvider = 'dockerhub' | 'ghcr' | 'gcr' | 'ecr' | 'acr' | 'custom'; + const [renderImageUrl, setRenderImageUrl] = useState('docker.io/ckoliveira/dataforge:latest'); + const [renderRegistry, setRenderRegistry] = useState('dockerhub'); + const [renderRegistryInputs, setRenderRegistryInputs] = useState({ + dockerhubUser: '', + dockerhubImage: 'dataforge', + dockerhubTag: 'latest', + ghcrUser: '', + ghcrImage: 'dataforge', + ghcrTag: 'latest', + gcrProject: '', + gcrImage: 'dataforge', + gcrTag: 'latest', + ecrAccount: '', + ecrRegion: 'us-east-1', + ecrImage: 'dataforge', + ecrTag: 'latest', + acrRegistry: '', + acrImage: 'dataforge', + acrTag: 'latest', + customUrl: '', + }); + const [showRenderExport, setShowRenderExport] = useState(null); + + const REGISTRY_META: Record string; loginCmd: string; pushCmd: (url: string) => string }> = { + dockerhub: { + label: 'Docker Hub', + color: '#2496ed', + buildUrl: i => `docker.io/${i.dockerhubUser || ''}/${i.dockerhubImage}:${i.dockerhubTag}`, + loginCmd: 'docker login', + pushCmd: url => `docker build -t ${url} .\ndocker push ${url}`, + }, + ghcr: { + label: 'GitHub Container Registry', + color: '#6e5494', + buildUrl: i => `ghcr.io/${i.ghcrUser || ''}/${i.ghcrImage}:${i.ghcrTag}`, + loginCmd: 'echo $GITHUB_TOKEN | docker login ghcr.io -u --password-stdin', + pushCmd: url => `docker build -t ${url} .\ndocker push ${url}`, + }, + gcr: { + label: 'Google Container Registry', + color: '#4285f4', + buildUrl: i => `gcr.io/${i.gcrProject || ''}/${i.gcrImage}:${i.gcrTag}`, + loginCmd: 'gcloud auth configure-docker', + pushCmd: url => `docker build -t ${url} .\ndocker push ${url}`, + }, + ecr: { + label: 'AWS ECR', + color: '#ff9900', + buildUrl: i => `${i.ecrAccount || ''}.dkr.ecr.${i.ecrRegion}.amazonaws.com/${i.ecrImage}:${i.ecrTag}`, + loginCmd: 'aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com', + pushCmd: url => `docker build -t ${url} .\ndocker push ${url}`, + }, + acr: { + label: 'Azure Container Registry', + color: '#0089d6', + buildUrl: i => `${i.acrRegistry || ''}.azurecr.io/${i.acrImage}:${i.acrTag}`, + loginCmd: 'az acr login --name ', + pushCmd: url => `docker build -t ${url} .\ndocker push ${url}`, + }, + custom: { + label: 'Custom URL', + color: 'var(--text-muted)', + buildUrl: i => i.customUrl || '/image:tag', + loginCmd: 'docker login ', + pushCmd: url => `docker build -t ${url} .\ndocker push ${url}`, + }, + }; + + const computedImageUrl = (): string => { + const m = REGISTRY_META[renderRegistry]; + return m.buildUrl(renderRegistryInputs); + }; + + const buildRenderYaml = (s: ScheduleDef, imageUrl: string): string => { + const cfg = s.config; + const fmt = (cfg.formats ?? ['csv']).join(' --format '); + const cmdParts = [ + 'dataset-gen generate', + '--domain custom', + '--config /app/schedule.yaml', + `--format ${fmt}`, + ]; + if (cfg.rows) cmdParts.push(`--rows ${cfg.rows}`); + if (cfg.tables?.length) cmdParts.push(...cfg.tables.map((t: string) => `--tables ${t}`)); + if (cfg.uploadTarget) { + cmdParts.push(`--upload ${cfg.uploadTarget}`); + cmdParts.push('--bucket ${CLOUD_BUCKET}'); + cmdParts.push('--prefix ${CLOUD_PREFIX}'); + } + if (cfg.dbUrl) cmdParts.push('--db-url ${DB_URL}'); + if (cfg.dateAnchors?.length) { + for (const a of cfg.dateAnchors) { + cmdParts.push(`--increment ${a.table}:${a.column}:${a.offsetDays}:days`); + } + } + + const envVars: string[] = [ + ` - key: PYTHONPATH\n value: /app/src`, + ]; + if (cfg.uploadTarget) { + envVars.push(` - key: CLOUD_BUCKET\n value: "${cfg.bucket ?? ''}"`); + envVars.push(` - key: CLOUD_PREFIX\n value: "${cfg.prefix ?? 'datasets/'}"`); + if (cfg.uploadTarget === 'gcs') { + envVars.push(` # GCS: add GOOGLE_APPLICATION_CREDENTIALS via Secret Group`); + } else if (cfg.uploadTarget === 's3') { + envVars.push(` - key: AWS_ACCESS_KEY_ID\n sync: false`); + envVars.push(` - key: AWS_SECRET_ACCESS_KEY\n sync: false`); + } else if (cfg.uploadTarget === 'azure') { + envVars.push(` - key: AZURE_STORAGE_CONNECTION_STRING\n sync: false`); + } + } + if (cfg.dbUrl) { + envVars.push(` - key: DB_URL\n sync: false # set in Render dashboard`); + } + + return `# Render Blueprint — generated from schedule "${s.name}" +# Docs: https://render.com/docs/blueprint-spec + +services: + - type: cron + name: ${s.name.toLowerCase().replace(/\s+/g, '-')} + image: + url: ${imageUrl} + schedule: "${s.cronExpression}" + dockerCommand: >- + ${cmdParts.join(' \\\n ')} + envVars: +${envVars.join('\n')} +`; + }; + + const handleDownloadRenderYaml = (s: ScheduleDef, imageUrl: string) => { + const content = buildRenderYaml(s, imageUrl); + const blob = new Blob([content], { type: 'text/yaml' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `render-${s.name.toLowerCase().replace(/\s+/g, '-')}.yaml`; + a.click(); + URL.revokeObjectURL(url); + setShowRenderExport(null); + }; + + const openEditSchedule = (s: ScheduleDef) => { + setEditingSchedule(s); + const cfg = s.config; + setSchedForm({ + name: s.name, + cronExpression: s.cronExpression, + destination: cfg.uploadTarget ? 'cloud' : cfg.dbUrl ? 'database' : 'local', + formats: cfg.formats ?? ['csv'], + outputDir: cfg.outputDir ?? 'output', + rows: cfg.rows != null ? String(cfg.rows) : '', + jsonMode: cfg.jsonMode ?? 'flat', + uploadTarget: cfg.uploadTarget ?? 'gcs', + bucket: cfg.bucket ?? '', + prefix: cfg.prefix ?? 'datasets/', + dbUrl: cfg.dbUrl ?? '', + ifExists: cfg.ifExists ?? 'append', + dbSchema: cfg.dbSchema ?? '', + tablesToInclude: cfg.tables ?? [], + cloudCreds: cfg.cloudCreds ?? { gcsJson: '', s3AccessKey: '', s3SecretKey: '', s3Region: 'us-east-1', azureConnStr: '' }, + dateAnchors: (cfg.dateAnchors ?? []).map((a: any) => ({ ...a, offsetDays: String(a.offsetDays) })), + }); + setSchedError(''); + // sync visual builder from cron expression + const parts = s.cronExpression.trim().split(/\s+/); + if (parts.length >= 4) setSchedVisual({ minute: parts[0], hour: parts[1], day: parts[2], month: parts[3] }); + setSchedBuilderMode('visual'); + setShowScheduleForm(true); + }; + + const openNewSchedule = () => { + setEditingSchedule(null); + setSchedVisual({ minute: '0', hour: '9', day: '*', month: '*' }); + setSchedBuilderMode('visual'); + setSchedForm({ + name: '', + cronExpression: '0 9 * * *', + destination: 'local', + formats: ['csv'], + outputDir: 'output', + rows: '', + jsonMode: 'flat', + uploadTarget: 'gcs', + bucket: '', + prefix: 'datasets/', + dbUrl: '', + ifExists: 'append', + dbSchema: '', + tablesToInclude: [], + cloudCreds: { gcsJson: '', s3AccessKey: '', s3SecretKey: '', s3Region: 'us-east-1', azureConnStr: '' }, + dateAnchors: [], + }); + setSchedError(''); + setShowScheduleForm(true); + }; + + const cronDescription = (expr: string): string => { + const parts = expr.trim().split(/\s+/); + if (parts.length !== 5) return ''; + const [min, hr, dom, mon, dow] = parts; + if (dom === '*' && mon === '*' && dow === '*') { + if (hr === '*' && min === '*') return 'Every minute'; + if (hr === '*') return `Every hour at :${min.padStart(2,'0')}`; + const hrN = parseInt(hr); const minN = parseInt(min); + if (!isNaN(hrN) && !isNaN(minN)) { + const ap = hrN >= 12 ? 'PM' : 'AM'; + const h = hrN % 12 || 12; + const m = String(minN).padStart(2,'0'); + return `Every day at ${h}:${m} ${ap} UTC`; + } + } + if (dow !== '*' && dom === '*' && mon === '*') { + const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; + const d = days[parseInt(dow)]; + if (d) { + const hrN = parseInt(hr); const minN = parseInt(min); + if (!isNaN(hrN) && !isNaN(minN)) { + const ap = hrN >= 12 ? 'PM' : 'AM'; + const h = hrN % 12 || 12; + return `Every ${d} at ${h}:${String(minN).padStart(2,'0')} ${ap} UTC`; + } + } + } + return ''; + }; + const [showRunPanel, setShowRunPanel] = useState(false); const [showRunHelp, setShowRunHelp] = useState(false); const [canBrowseFolder, setCanBrowseFolder] = useState(false); @@ -668,6 +1327,9 @@ export default function App() { await fetch(`/api/credential-profiles/${encodeURIComponent(name)}`, { method: 'DELETE' }); fetchCredProfiles(); }; + const RUN_CREDS_STORAGE = 'dataforge_run_creds'; + const loadRunCreds = () => { try { return JSON.parse(localStorage.getItem(RUN_CREDS_STORAGE) || '{}'); } catch { return {}; } }; + const [runConfig, setRunConfig] = useState<{ formats: string[], destination: 'local' | 'cloud' | 'database', @@ -708,7 +1370,7 @@ export default function App() { partitionDateGranularity: {}, jsonMode: 'flat', seed: '', - dbUrl: '', + dbUrl: loadRunCreds().dbUrl ?? '', ifExists: 'replace', dbSchema: '', recurrence: '', @@ -718,15 +1380,62 @@ export default function App() { increments: [], workers: '16', cloudCreds: { - gcsJson: '', - s3AccessKey: '', - s3SecretKey: '', - s3Region: 'us-east-1', - azureConnStr: '', + gcsJson: loadRunCreds().cloudCreds?.gcsJson ?? '', + s3AccessKey: loadRunCreds().cloudCreds?.s3AccessKey ?? '', + s3SecretKey: loadRunCreds().cloudCreds?.s3SecretKey ?? '', + s3Region: loadRunCreds().cloudCreds?.s3Region ?? 'us-east-1', + azureConnStr: loadRunCreds().cloudCreds?.azureConnStr ?? '', }, }); const computedDbUrl = dbAdvanced ? runConfig.dbUrl : buildDbUrl(dbForm); + + // ── Persist credential key refs to localStorage ──────────────────────────── + useEffect(() => { + localStorage.setItem(RUN_CREDS_STORAGE, JSON.stringify({ dbUrl: runConfig.dbUrl, cloudCreds: runConfig.cloudCreds })); + }, [runConfig.dbUrl, runConfig.cloudCreds]); + + // ── Sync env-keys → auto-select key NAMES (not values) in AI / Cloud / DB ─── + // Fields store the env key NAME (e.g. "AWS_ACCESS_KEY_ID"); values resolved at send time. + useEffect(() => { + if (Object.keys(envKeys).length === 0) return; + + // AI: auto-select the first matching key name for each provider (only if not already set) + const aiKeyMap: Record = { + ANTHROPIC_API_KEY: 'anthropic', OPENAI_API_KEY: 'openai', + GOOGLE_API_KEY: 'google', GEMINI_API_KEY: 'google', + GROQ_API_KEY: 'groq', MISTRAL_API_KEY: 'mistral', + TOGETHER_API_KEY: 'together', + }; + setAiApiKeys(prev => { + const next = { ...prev }; + for (const [envVar, provider] of Object.entries(aiKeyMap)) { + if (envKeys[envVar] && !next[provider]) next[provider] = envVar; // store the NAME + } + return next; + }); + + // Cloud: auto-select key names (only if field is empty) + const defaultCloudNames: Partial> = { + s3AccessKey: envKeys['AWS_ACCESS_KEY_ID'] ? 'AWS_ACCESS_KEY_ID' : undefined, + s3SecretKey: envKeys['AWS_SECRET_ACCESS_KEY'] ? 'AWS_SECRET_ACCESS_KEY' : undefined, + s3Region: envKeys['AWS_DEFAULT_REGION'] ? 'AWS_DEFAULT_REGION' + : envKeys['AWS_REGION'] ? 'AWS_REGION' : undefined, + gcsJson: envKeys['GCS_JSON'] ? 'GCS_JSON' + : envKeys['GOOGLE_APPLICATION_CREDENTIALS_JSON'] ? 'GOOGLE_APPLICATION_CREDENTIALS_JSON' : undefined, + azureConnStr: envKeys['AZURE_STORAGE_CONNECTION_STRING'] ? 'AZURE_STORAGE_CONNECTION_STRING' : undefined, + }; + setRunConfig(r => { + const creds = { ...r.cloudCreds }; + for (const [field, name] of Object.entries(defaultCloudNames)) { + if (name && !(r.cloudCreds as any)[field]) (creds as any)[field] = name; + } + const dbUrl = !r.dbUrl && envKeys['DATABASE_URL'] ? 'DATABASE_URL' : r.dbUrl; + return { ...r, cloudCreds: creds, dbUrl }; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [envKeys]); + const [runLogs, setRunLogs] = useState(''); const [isRunning, setIsRunning] = useState(false); const runAbortRef = useRef(null); @@ -754,7 +1463,9 @@ export default function App() { partitionDateGranularity: Object.keys(runConfig.partitionDateGranularity).length > 0 ? runConfig.partitionDateGranularity : undefined, jsonMode: runConfig.jsonMode, seed: runConfig.seed !== '' ? parseInt(runConfig.seed) : undefined, - dbUrl: runConfig.destination === 'database' ? computedDbUrl || undefined : undefined, + dbUrl: runConfig.destination === 'database' + ? (dbAdvanced ? (envKeys[runConfig.dbUrl] ?? runConfig.dbUrl) : computedDbUrl) || undefined + : undefined, ifExists: runConfig.ifExists, dbSchema: runConfig.dbSchema || undefined, recurrence: runConfig.recurrence !== '' ? parseFloat(runConfig.recurrence) : undefined, @@ -763,7 +1474,13 @@ export default function App() { columns: runConfig.columnsFilter.trim() ? runConfig.columnsFilter.trim().split('\n').filter(Boolean) : undefined, increments: runConfig.increments.filter(i => i.table && i.column && i.step !== ''), workers: runConfig.workers !== '' ? parseInt(runConfig.workers) : 16, - cloudCreds: runConfig.destination === 'cloud' ? runConfig.cloudCreds : undefined, + cloudCreds: runConfig.destination === 'cloud' ? { + gcsJson: envKeys[runConfig.cloudCreds.gcsJson] ?? runConfig.cloudCreds.gcsJson, + s3AccessKey: envKeys[runConfig.cloudCreds.s3AccessKey] ?? '', + s3SecretKey: envKeys[runConfig.cloudCreds.s3SecretKey] ?? '', + s3Region: envKeys[runConfig.cloudCreds.s3Region] ?? runConfig.cloudCreds.s3Region, + azureConnStr: envKeys[runConfig.cloudCreds.azureConnStr] ?? '', + } : undefined, }) }); @@ -787,26 +1504,25 @@ export default function App() { if (msg.type === 'cmd' || msg.type === 'out') { setRunLogs(prev => prev + msg.text); } else if (msg.type === 'done') { - const statusLine = msg.stopped ? '\n⏹ Stopped.' : msg.success ? '\n✓ Done.' : '\n✗ Failed.'; + const statusLine = msg.stopped ? '\n' + t('run.stopped') : msg.success ? '\n' + t('run.done') : '\n' + t('run.failed'); setRunLogs(prev => prev + statusLine); - setIsRunning(false); } } catch {} } } } catch (e: any) { if ((e as DOMException).name !== 'AbortError') { - setRunLogs(prev => prev + `\nConnection Error: ${e.message || String(e)}`); + setRunLogs(prev => prev + `\n${t('run.connectionError')} ${e.message || String(e)}`); } - setIsRunning(false); } finally { + setIsRunning(false); runAbortRef.current = null; } }; const handleStopCli = async () => { await fetch('/api/stop-cli', { method: 'POST' }); - setRunLogs(prev => prev + '\n⏹ Stop requested…'); + setRunLogs(prev => prev + '\n' + t('run.stopRequested')); }; // Sync position changes back to tables state dynamically when nodes are dragged @@ -825,7 +1541,7 @@ export default function App() { Dataforge - Synthetic Dataset Generator + {t('auth.subtitle')}
@@ -839,7 +1555,7 @@ export default function App() { - Docs + {t('nav.docs')} - GitHub + {t('nav.github')} + + {/* User badge */} +
+ {/* Language toggle */} +
i18n.changeLanguage(i18n.language === 'pt-BR' ? 'en' : 'pt-BR')} + title={t('common.language')} + style={{ + display: 'flex', + alignItems: 'center', + background: 'rgba(255,255,255,0.06)', + border: '1px solid rgba(255,255,255,0.12)', + borderRadius: '999px', + padding: '0.18rem', + cursor: 'pointer', + position: 'relative', + gap: '0.1rem', + userSelect: 'none', + }} + > + {(['pt-BR', 'en'] as const).map(lang => ( + + {lang === 'pt-BR' ? '🇧🇷' : '🇺🇸'} + {lang === 'pt-BR' ? 'PT-BR' : 'EN'} + + ))} +
+ + +
- + @@ -878,21 +1649,24 @@ export default function App() { )}
+
{tables.length > 0 && ( )} {tables.length > 0 && ( )} {tables.length > 0 && ( )} {tables.length > 0 && ( )} +
+ + {t('nav.comingSoon')} +
+
+ + {t('nav.comingSoon')} +
-
+
@@ -972,31 +1758,31 @@ export default function App() { boxShadow: '-8px 0 32px rgba(0,0,0,0.5)' }}>
-

Edit Table

+

{t('schema.editTable')}

-
- - onUpdateTable(selectedTable.id, 'name', e.target.value)} + + onUpdateTable(selectedTable.id, 'name', e.target.value)} />
- +
-

Columns

+

{t('schema.columns')}

{selectedTable.columns.length === 0 ? ( -
- No columns defined. +
+ {t('schema.noColumnsDefined')}
) : ( selectedTable.columns.map(col => ( -
+
- onUpdateColumn(selectedTable.id, col.id, 'name', e.target.value)} - placeholder="Column Name" + placeholder={t('schema.columnName')} style={{ flex: 1, marginRight: '0.5rem', padding: '0.5rem' }} /> @@ -1039,7 +1825,7 @@ export default function App() {
- +
- +
{parseFloat(col.nullable as any) > 0 ? ( @@ -1072,10 +1858,10 @@ export default function App() { }} style={{ width: '52px', padding: '0.3rem 0.4rem', fontSize: '0.8rem', textAlign: 'center' }} /> - % + %
) : ( - No + {t('schema.no')} )}
@@ -1084,20 +1870,20 @@ export default function App() { {['int', 'float', 'date'].includes(col.dtype) && (
- + onUpdateColumn(selectedTable.id, col.id, 'min', e.target.value)} - placeholder={col.dtype === 'date' ? 'e.g. -1y' : 'e.g. 0'} + placeholder={col.dtype === 'date' ? t('schema.datePlaceholderMin') : t('schema.numericPlaceholderMin')} style={{ padding: '0.5rem', width: '100%', boxSizing: 'border-box' }} />
- + onUpdateColumn(selectedTable.id, col.id, 'max', e.target.value)} - placeholder={col.dtype === 'date' ? 'e.g. today' : 'e.g. 100'} + placeholder={col.dtype === 'date' ? t('schema.datePlaceholderMax') : t('schema.numericPlaceholderMax')} style={{ padding: '0.5rem', width: '100%', boxSizing: 'border-box' }} />
@@ -1110,13 +1896,13 @@ export default function App() { onClick={() => { onUpdateColumn(selectedTable.id, col.id, 'choices', []); }} style={{ flex: 1, padding: '0.3rem', fontSize: '0.75rem', borderRadius: '4px', border: `1px solid ${col.choices.length === 0 ? 'rgba(34,211,238,0.4)' : 'transparent'}`, cursor: 'pointer', background: col.choices.length === 0 ? 'rgba(34,211,238,0.12)' : 'rgba(255,255,255,0.05)', color: col.choices.length === 0 ? 'var(--primary)' : 'var(--text-muted)' }} > - Faker Method + {t('schema.fakerMethod')}
@@ -1133,18 +1919,18 @@ export default function App() { }} onFocus={() => setFakerDropdown({ tableId: selectedTable.id, colId: col.id })} onBlur={() => setTimeout(() => setFakerDropdown(null), 150)} - placeholder="e.g. phone_number" + placeholder={t('schema.fakerMethodPlaceholder')} style={{ padding: '0.5rem', paddingRight: '1.8rem' }} /> {col.fakerProvider && ( )}
))}
@@ -1185,7 +1971,7 @@ export default function App() {
{ if (e.key === 'Enter' || e.key === ',') { @@ -1198,7 +1984,7 @@ export default function App() { } }} /> -

Press Enter or comma to add

+

{t('schema.pressEnterOrComma')}

)}
@@ -1206,32 +1992,32 @@ export default function App() {
{col.isForeignKey && ( -
+
- - onUpdateColumn(selectedTable.id, col.id, 'fkTable', e.target.value)} - style={{ padding: '0.5rem', border: '1px solid rgba(236, 72, 153, 0.3)' }} + + onUpdateColumn(selectedTable.id, col.id, 'fkTable', e.target.value)} + style={{ padding: '0.5rem', border: '1px solid rgba(251,146,60,0.3)' }} />
- - onUpdateColumn(selectedTable.id, col.id, 'fkColumn', e.target.value)} - style={{ padding: '0.5rem', border: '1px solid rgba(236, 72, 153, 0.3)' }} + + onUpdateColumn(selectedTable.id, col.id, 'fkColumn', e.target.value)} + style={{ padding: '0.5rem', border: '1px solid rgba(251,146,60,0.3)' }} />
@@ -1250,8 +2036,8 @@ export default function App() {
setShowRunHelp(false)}>
e.stopPropagation()}>
-

Run Generator — Field Reference

- +

{t('run.fieldReference')}

+
{([ { section: 'Format', fields: [ @@ -1283,11 +2069,11 @@ export default function App() { ]}, ] as { section: string; fields: { name: string; desc: string }[] }[]).map(({ section, fields }) => (
-

{section}

+

{section}

{fields.map(({ name, desc }) => ( -
-

{name}

-

{desc}

+
+

{name}

+

{desc}

))}
@@ -1301,9 +2087,9 @@ export default function App() {
-

Run Generator

+

{t('run.title')}

- +
@@ -1312,7 +2098,7 @@ export default function App() { {/* Formats + JSON mode */} {runConfig.destination !== 'database' &&
-

Format

+

{t('run.format')}

{(['csv', 'json', 'parquet', 'avro'] as const).map(fmt => { const active = runConfig.formats.includes(fmt); @@ -1324,7 +2110,7 @@ export default function App() { : [...runConfig.formats, fmt]; setRunConfig(r => ({ ...r, formats: next.length > 0 ? next : [fmt] })); }} - style={{ padding: '0.35rem 0.85rem', borderRadius: '6px', border: `1px solid ${active ? '#10b981' : 'rgba(255,255,255,0.1)'}`, background: active ? 'rgba(16,185,129,0.15)' : 'rgba(255,255,255,0.05)', color: active ? '#10b981' : '#94a3b8', cursor: 'pointer', fontSize: '0.85rem', fontWeight: active ? 600 : 400 }}> + style={{ padding: '0.35rem 0.85rem', borderRadius: '6px', border: `1px solid ${active ? 'var(--success)' : 'var(--border-color)'}`, background: active ? 'rgba(74,222,128,0.12)' : 'rgba(255,255,255,0.05)', color: active ? 'var(--success)' : 'var(--text-muted)', cursor: 'pointer', fontSize: '0.85rem', fontWeight: active ? 600 : 400 }}> {fmt.toUpperCase()} ); @@ -1332,16 +2118,16 @@ export default function App() {
{runConfig.formats.includes('json') && (
- +
)}
- + setRunConfig(r => ({...r, rows: e.target.value}))} style={{ width: '100%', padding: '0.5rem' }} placeholder="e.g. 5000" min="1" />
@@ -1349,18 +2135,18 @@ export default function App() { {/* Destination */}
-

Destination

+

{t('run.destination')}

{([ - { key: 'local', label: 'Local', color: '#60a5fa' }, - { key: 'cloud', label: 'Cloud', color: '#a78bfa' }, - { key: 'database', label: 'Database', color: '#f59e0b' }, + { key: 'local', label: t('run.local'), color: '#60a5fa' }, + { key: 'cloud', label: t('run.cloud'), color: '#a78bfa' }, + { key: 'database', label: t('run.database'), color: '#f59e0b' }, ] as const).map(({ key, label, color }) => { const active = runConfig.destination === key; return ( ); @@ -1370,7 +2156,7 @@ export default function App() { {/* Local */} {runConfig.destination === 'local' && (
- +
setRunConfig(r => ({...r, outputDir: e.target.value}))} style={{ flex: 1, padding: '0.5rem' }} placeholder="e.g. output" /> {canBrowseFolder && } @@ -1404,17 +2190,17 @@ export default function App() { {/* Saved credential profiles */} {credProfiles.filter(p => p.provider === runConfig.uploadTarget).length > 0 && (
- +
{credProfiles.filter(p => p.provider === runConfig.uploadTarget).map(p => (
))} @@ -1424,22 +2210,22 @@ export default function App() { {/* Provider */}
- +
{/* Bucket + Prefix */}
- + setRunConfig(r => ({...r, bucket: e.target.value}))} style={{ width: '100%', padding: '0.5rem' }} placeholder="e.g. my-data-lake" />
- + setRunConfig(r => ({...r, prefix: e.target.value}))} style={{ width: '100%', padding: '0.5rem' }} placeholder="e.g. datasets/" />
@@ -1447,7 +2233,7 @@ export default function App() { {/* Credentials — per provider */}
-

Credentials

+

Credentials

{showSaveCredInput ? (
+ style={{ background: 'none', border: 'none', color: 'var(--text-subtle)', cursor: 'pointer', fontSize: '0.85rem' }}>✕
) : ( )}
{runConfig.uploadTarget === 'gcs' && ( -
- -