From 46af290db394332d6c4d1e40bfaf88879b296ed0 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 7 Nov 2026 16:39:43 +0300 Subject: [PATCH] initial commit --- .cursorrules | 23 + .env.example | 43 + .gitea/workflows/ci.yml | 73 + .gitea/workflows/deploy.yml | 28 + .gitignore | 28 + .rules | 64 + AGENTS.md | 20 + PROJECT.md | 49 + README.md | 88 + STATUS.md | 144 + TASKS.md | 258 + backend/.dockerignore | 40 + backend/.env.docker | 18 + backend/.env.example | 18 + backend/.env.local | 8 + backend/.gitignore | 11 + backend/Dockerfile | 49 + backend/README.md | 91 + backend/config/models.json | 72 + backend/config/settings.py | 48 + backend/main.py | 44 + backend/models/__init__.py | 0 backend/models/ai.py | 95 + backend/models/bible.py | 46 + backend/models/document.py | 51 + backend/models/project.py | 15 + backend/models/user.py | 28 + backend/requirements.txt | 8 + backend/routers/__init__.py | 1 + backend/routers/ai.py | 428 + backend/routers/auth.py | 50 + backend/routers/bible.py | 83 + backend/routers/documents.py | 151 + backend/routers/export.py | 64 + backend/routers/health.py | 8 + backend/routers/images.py | 40 + backend/routers/projects.py | 85 + backend/services/__init__.py | 0 backend/services/auth_service.py | 109 + backend/services/bible_service.py | 135 + backend/services/context_builder.py | 123 + backend/services/deduplication.py | 133 + backend/services/document_service.py | 302 + backend/services/entity_extractor.py | 134 + backend/services/export_service.py | 239 + backend/services/image_generator.py | 203 + backend/services/llm_router.py | 126 + backend/services/model_config.py | 47 + backend/services/presets.py | 85 + backend/services/project_service.py | 196 + backend/test_docker.sh | 20 + docker-compose.yml | 64 + frontend/.dockerignore | 38 + frontend/.gitignore | 41 + frontend/AGENTS.md | 9 + frontend/Dockerfile | 55 + frontend/README.md | 36 + frontend/eslint.config.mjs | 18 + frontend/next.config.js | 7 + frontend/next.config.ts | 17 + frontend/package-lock.json | 7619 +++++++++++++++++ frontend/package.json | 38 + frontend/public/file.svg | 1 + frontend/public/globe.svg | 1 + frontend/public/next.svg | 1 + frontend/public/vercel.svg | 1 + frontend/public/window.svg | 1 + frontend/src/app/diff-test/page.tsx | 5 + frontend/src/app/favicon.ico | Bin 0 -> 15086 bytes frontend/src/app/globals.css | 27 + frontend/src/app/layout.tsx | 29 + frontend/src/app/page.tsx | 42 + frontend/src/app/test/page.tsx | 11 + frontend/src/components/auth/AuthScreen.tsx | 135 + frontend/src/components/editor/AIDiffView.tsx | 71 + frontend/src/components/editor/AIMenu.tsx | 116 + .../src/components/editor/AddToBibleModal.tsx | 237 + .../components/editor/BubbleMenuWrapper.tsx | 56 + frontend/src/components/editor/Editor.tsx | 480 ++ .../src/components/editor/ExportModal.tsx | 127 + .../components/editor/GenerateImageModal.tsx | 203 + frontend/src/components/editor/Toolbar.tsx | 132 + .../src/components/settings/ModelSettings.tsx | 203 + .../src/components/sidebar/DocumentTree.tsx | 357 + .../src/components/sidebar/FileExplorer.tsx | 16 + frontend/src/components/sidebar/LeftPanel.tsx | 30 + .../src/components/sidebar/ProjectManager.tsx | 174 + .../components/sidebar/StoryBiblePanel.tsx | 547 ++ .../src/components/sidebar/StoryExplorer.tsx | 14 + frontend/src/components/test/AITest.tsx | 90 + frontend/src/components/test/DiffTest.tsx | 36 + frontend/src/lib/ai-api.ts | 197 + frontend/src/lib/api.ts | 471 + .../src/lib/tiptap-extensions/FontSize.ts | 88 + frontend/src/stores/appStore.ts | 422 + frontend/test_docker.sh | 15 + frontend/tsconfig.json | 34 + setup.sh | 94 + start.sh | 31 + stop.sh | 4 + 100 files changed, 16663 insertions(+) create mode 100644 .cursorrules create mode 100644 .env.example create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 .rules create mode 100644 AGENTS.md create mode 100644 PROJECT.md create mode 100644 README.md create mode 100644 STATUS.md create mode 100644 TASKS.md create mode 100644 backend/.dockerignore create mode 100644 backend/.env.docker create mode 100644 backend/.env.example create mode 100644 backend/.env.local create mode 100644 backend/.gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/README.md create mode 100644 backend/config/models.json create mode 100644 backend/config/settings.py create mode 100644 backend/main.py create mode 100644 backend/models/__init__.py create mode 100644 backend/models/ai.py create mode 100644 backend/models/bible.py create mode 100644 backend/models/document.py create mode 100644 backend/models/project.py create mode 100644 backend/models/user.py create mode 100644 backend/requirements.txt create mode 100644 backend/routers/__init__.py create mode 100644 backend/routers/ai.py create mode 100644 backend/routers/auth.py create mode 100644 backend/routers/bible.py create mode 100644 backend/routers/documents.py create mode 100644 backend/routers/export.py create mode 100644 backend/routers/health.py create mode 100644 backend/routers/images.py create mode 100644 backend/routers/projects.py create mode 100644 backend/services/__init__.py create mode 100644 backend/services/auth_service.py create mode 100644 backend/services/bible_service.py create mode 100644 backend/services/context_builder.py create mode 100644 backend/services/deduplication.py create mode 100644 backend/services/document_service.py create mode 100644 backend/services/entity_extractor.py create mode 100644 backend/services/export_service.py create mode 100644 backend/services/image_generator.py create mode 100644 backend/services/llm_router.py create mode 100644 backend/services/model_config.py create mode 100644 backend/services/presets.py create mode 100644 backend/services/project_service.py create mode 100755 backend/test_docker.sh create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/.gitignore create mode 100644 frontend/AGENTS.md create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.mjs create mode 100644 frontend/next.config.js create mode 100644 frontend/next.config.ts create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/file.svg create mode 100644 frontend/public/globe.svg create mode 100644 frontend/public/next.svg create mode 100644 frontend/public/vercel.svg create mode 100644 frontend/public/window.svg create mode 100644 frontend/src/app/diff-test/page.tsx create mode 100644 frontend/src/app/favicon.ico create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/src/app/test/page.tsx create mode 100644 frontend/src/components/auth/AuthScreen.tsx create mode 100644 frontend/src/components/editor/AIDiffView.tsx create mode 100644 frontend/src/components/editor/AIMenu.tsx create mode 100644 frontend/src/components/editor/AddToBibleModal.tsx create mode 100644 frontend/src/components/editor/BubbleMenuWrapper.tsx create mode 100644 frontend/src/components/editor/Editor.tsx create mode 100644 frontend/src/components/editor/ExportModal.tsx create mode 100644 frontend/src/components/editor/GenerateImageModal.tsx create mode 100644 frontend/src/components/editor/Toolbar.tsx create mode 100644 frontend/src/components/settings/ModelSettings.tsx create mode 100644 frontend/src/components/sidebar/DocumentTree.tsx create mode 100644 frontend/src/components/sidebar/FileExplorer.tsx create mode 100644 frontend/src/components/sidebar/LeftPanel.tsx create mode 100644 frontend/src/components/sidebar/ProjectManager.tsx create mode 100644 frontend/src/components/sidebar/StoryBiblePanel.tsx create mode 100644 frontend/src/components/sidebar/StoryExplorer.tsx create mode 100644 frontend/src/components/test/AITest.tsx create mode 100644 frontend/src/components/test/DiffTest.tsx create mode 100644 frontend/src/lib/ai-api.ts create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/tiptap-extensions/FontSize.ts create mode 100644 frontend/src/stores/appStore.ts create mode 100755 frontend/test_docker.sh create mode 100644 frontend/tsconfig.json create mode 100755 setup.sh create mode 100755 start.sh create mode 100755 stop.sh diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..0606576 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,23 @@ +# Правила разработки Writer IDE + +## Общие принципы +- Ты — опытный Fullstack разработчик (Next.js + FastAPI). +- Всегда пиши типизированный TypeScript код. Избегай `any`. +- Для стилизации используй ТОЛЬКО Tailwind CSS. Никаких inline-стилей или CSS-модулей, если это не критично. +- Состояние приложения управляется через Zustand. +- API-запросы типизированы и вынесены в `frontend/src/lib/api.ts`. + +## Frontend (Next.js App Router) +- Компоненты должны быть по возможности 'use client' только если используются хуки или интерактивность. +- Структура компонента: Импорты -> Типы -> Компонент -> Экспорт. +- Используй Lucide React для иконок. + +## Backend (FastAPI) +- Используй асинхронные функции (`async def`) для всех эндпоинтов. +- Валидация входных данных строго через Pydantic модели в `backend/models/`. +- Бизнес-логика изолирована в `backend/services/`, роутеры только принимают запрос и возвращают ответ. + +## Процесс работы +1. Прежде чем писать код, кратко опиши план действий (1-3 предложения). +2. Пиши код модульно. Если файл превышает 200 строк, предложи разбить его. +3. После генерации кода, укажи, как его протестировать или проверить. \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..893cdca --- /dev/null +++ b/.env.example @@ -0,0 +1,43 @@ +# ============================================ +# Writer IDE - Docker Configuration +# Скопируйте этот файл в .env и заполните значения +# ============================================ + +# --- OpenRouter API --- +OPENROUTER_API_KEY=your_api_key_here +OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 + +# --- Ollama --- +# URL Ollama сервера. Варианты: +# - Если Ollama в другом Docker-контейнере на этом сервере: +# http://host.docker.internal:11434 (Linux/macOS/Windows) +# - Если Ollama на том же хосте, но не в Docker: +# http://host.docker.internal:11434 +# - Если Ollama в той же Docker-сети: +# http://ollama:11434 +OLLAMA_BASE_URL=http://host.docker.internal:11434 + +# --- Путь к данным на HDD --- +# Путь на хосте, куда будут сохраняться все данные проекта +# Убедитесь, что этот путь существует и имеет правильные права доступа +# Пример: /mnt/data/writer-ide или /data/writer-ide +DATA_VOLUME_PATH=/mnt/data/writer-ide + +# --- Frontend --- +# URL Backend API, который будет использоваться браузером +# Если доступ к серверу по IP: http://YOUR_SERVER_IP:8000/api +# Если через домен: http://your-domain.com:8000/api +NEXT_PUBLIC_API_URL=http://localhost:8000/api + +# --- Безопасность --- +# ОБЯЗАТЕЛЬНО измените в production! +# Сгенерировать можно командой: openssl rand -hex 32 +JWT_SECRET_KEY=change-this-to-a-long-random-secret-string + +# --- CORS --- +# Разрешённые источники для CORS (через запятую) +# Добавьте домен/IP вашего сервера +CORS_ORIGINS=http://localhost:3000,http://localhost:8000 + +# --- Окружение --- +ENVIRONMENT=production diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..b603b3e --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,73 @@ +name: Build and Push Docker Images + +on: + push: + branches: + - main + - master + tags: + - 'v*' + workflow_dispatch: # Возможность ручного запуска + +jobs: + build-and-push: + runs-on: ubuntu-latest # Или метка вашего раннера, например, 'self-hosted' или 'linux' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ gitea.server_url }} + username: ${{ gitea.actor }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Extract metadata (tags, labels) for Backend + id: meta-backend + uses: docker/metadata-action@v5 + with: + images: ${{ gitea.server_url }}/${{ gitea.repository }}/backend + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=sha- + + - name: Build and push Backend image + uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + push: true + tags: ${{ steps.meta-backend.outputs.tags }} + labels: ${{ steps.meta-backend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Extract metadata (tags, labels) for Frontend + id: meta-frontend + uses: docker/metadata-action@v5 + with: + images: ${{ gitea.server_url }}/${{ gitea.repository }}/frontend + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=sha- + + - name: Build and push Frontend image + uses: docker/build-push-action@v5 + with: + context: ./frontend + file: ./frontend/Dockerfile + push: true + tags: ${{ steps.meta-frontend.outputs.tags }} + labels: ${{ steps.meta-frontend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..6bd517d --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,28 @@ +name: Deploy to Server + +on: + workflow_run: + workflows: ["Build and Push Docker Images"] + types: + - completed + workflow_dispatch: + +jobs: + deploy: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest # Или ваш локальный раннер, если он имеет доступ к серверу + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + cd /path/to/writer-ide # Путь к папке с docker-compose.yml на сервере + docker-compose pull + docker-compose up -d + docker image prune -f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a39b57e --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Переменные окружения с секретами +.env + +# Данные (монтируются с HDD) +data/ +/mnt/data/ + +# Docker volumes (локальные) +volumes/ + +# IDE +.vscode/ +.idea/ +*.swp + +# Node +node_modules/ +.next/ + +# Python +__pycache__/ +*.pyc +venv/ +.venv/ + +# OS +.DS_Store +Thumbs.db diff --git a/.rules b/.rules new file mode 100644 index 0000000..5ba89ad --- /dev/null +++ b/.rules @@ -0,0 +1,64 @@ +# Правила разработки Writer IDE + +## Общие принципы +- Ты — опытный Fullstack разработчик (Next.js + FastAPI). +- Всегда пиши типизированный TypeScript код. Избегай `any`. +- Для стилизации используй ТОЛЬКО Tailwind CSS. Никаких inline-стилей или CSS-модулей, если это не критично. +- Состояние приложения управляется через Zustand. +- API-запросы типизированы и вынесены в `frontend/src/lib/api.ts`. + +## Frontend (Next.js App Router) +- Фреймворк: Next.js 14+ (App Router, TypeScript). +- Компоненты должны быть `'use client'` только если используются хуки или интерактивность. +- Структура компонента: Импорты -> Типы -> Компонент -> Экспорт. +- Используй Lucide React для иконок. +- Tailwind CSS v4: конфигурация через `@theme` в `globals.css` и `@plugin` для плагинов. +- Для форматирования текста в редакторе используй класс `prose` (плагин `@tailwindcss/typography`). + +## Tiptap Редактор +- Редактор: `@tiptap/react` с `StarterKit` и дополнительными расширениями. +- Базовые расширения: `TextStyle`, `Color`, `Underline`, `Blockquote`. +- Редактор должен быть обёрнут в `div` с классами `max-w-3xl mx-auto prose`. +- `onUpdate` вызывает экшен Zustand store для сохранения контента. + +## Backend (FastAPI) +- Фреймворк: FastAPI (Python 3.10+). +- Используй асинхронные функции (`async def`) для всех эндпоинтов. +- Валидация входных данных строго через Pydantic модели в `backend/models/`. +- Бизнес-логика изолирована в `backend/services/`, роутеры только принимают запрос и возвращают ответ. +- CORS: разрешён `http://localhost:3000`. + +## Управление состоянием (Zustand) +- Store располагается в `frontend/src/stores/`. +- Определяй интерфейсы для всех сущностей (Document, Project и т.д.). +- Инициализируй store с тестовыми данными для разработки. + +## AI Интеграция (внутренняя логика приложения) +- Роутинг: Абстрактный сервис, поддерживающий OpenRouter API и локальный Ollama (`http://localhost:11434`). +- Пресеты: Жесткое разделение параметров генерации (`temperature`, `top_p`) по действию (Creative Draft, Editor, Brainstorm). + +## Процесс работы +1. Прежде чем писать код, кратко опиши план действий (1-3 предложения). +2. Пиши код модульно. Если файл превышает 200 строк, предложи разбить его. +3. После генерации кода укажи, как его протестировать или проверить. + +## Структура проекта +``` +writer-ide/ +├── frontend/ # Next.js приложение +│ ├── src/ +│ │ ├── app/ # Next.js App Router (pages, layout) +│ │ ├── components/ # UI компоненты +│ │ │ ├── editor/ # Tiptap редактор, Toolbar, AI-меню +│ │ │ ├── sidebar/ # FileExplorer, StoryExplorer +│ │ │ └── ui/ # Переиспользуемые кнопки, инпуты +│ │ ├── lib/ # Утилиты, конфигурация axios, типы +│ │ └── stores/ # Zustand stores (appStore, editorStore) +│ └── package.json +│ +└── backend/ # FastAPI сервер + ├── main.py # Точка входа, настройка CORS + ├── routers/ # API эндпоинты (projects, documents, ai) + ├── services/ # Бизнес-логика (file_ops, llm_router) + └── models/ # Pydantic схемы запросов/ответов +``` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7db6312 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,20 @@ +# Стратегия использования AI для разработки Writer IDE + +## 🧠 Архитектор и сложная логика +- **Модель**: `qwen/qwen-2.5-coder-32b-instruct` (через OpenRouter) +- **Задачи**: Проектирование API, сложные хуки Zustand, настройка Tiptap, архитектура БД. +- **Параметры**: Temperature 0.2, Top_P 0.9. + +## 💻 Ежедневная разработка (Coding Agent) +- **Модель**: `qwen/qwen-2.5-coder-7b-instruct` (OpenRouter) или локальная `qwen2.5-coder:7b` (Ollama) для скорости. +- **Задачи**: Написание UI-компонентов, рутинный рефакторинг, написание типов TypeScript. +- **Параметры**: Temperature 0.1, Top_P 0.95. + +## 🔍 Код-ревью и отладка +- **Модель**: `qwen/qwen-2.5-72b-instruct` (OpenRouter) +- **Задачи**: Анализ трейсбеков, поиск логических ошибок, оптимизация производительности. +- **Параметры**: Temperature 0.0. + +## 🎨 Тестирование функционала (Внутренний AI приложения) +- **Модель**: Локальная `vikhr-creative-1.5b` или `qwen2.5:7b` через Ollama. +- **Задачи**: Проверка того, что наше приложение корректно отправляет промпты и получает креативные ответы. \ No newline at end of file diff --git a/PROJECT.md b/PROJECT.md new file mode 100644 index 0000000..042b8b2 --- /dev/null +++ b/PROJECT.md @@ -0,0 +1,49 @@ +# Writer IDE: Архитектурная спецификация + +## 🎯 Видение проекта +Создание локально-ориентированного веб-приложения (IDE для писателей), которое объединяет мощь rich-text редактирования, структурированное управление сюжетом (Story Bible) и бесшовную интеграцию AI-ассистента для генерации, редактуры и анализа текста. Приложение должно давать пользователю полный контроль над данными (локальное хранение) и гибкость в выборе AI-моделей (OpenRouter для качества, Ollama для приватности). + +## 🛠️ Технологический стек + +### Frontend +- **Фреймворк**: Next.js 14+ (App Router, TypeScript) +- **Стилизация**: Tailwind CSS (строгое использование utility-классов, никаких inline-styles) +- **Редактор**: Tiptap (Headless wrapper вокруг ProseMirror) +- **Управление состоянием**: Zustand (легковесный, без бойлерплейта Redux) +- **Иконки**: Lucide React +- **HTTP-клиент**: Axios (с типизацией ответов) + +### Backend +- **Фреймворк**: FastAPI (Python 3.10+) +- **Валидация**: Pydantic V2 +- **Асинхронность**: Строгое использование `async`/`await` +- **Хранение**: Локальная файловая система (JSON/Markdown файлы в директории проекта пользователя) + +### AI Интеграция (Внутренняя логика приложения) +- **Роутинг**: Абстрактный сервис, поддерживающий как OpenRouter API, так и локальный Ollama (`http://localhost:11434`). +- **Пресеты**: Жесткое разделение параметров генерации (`temperature`, `top_p`) в зависимости от действия (Creative Draft, Editor, Brainstorm). + +## 📁 Структура проекта +```text +writer-ide/ +├── PROJECT.md # Этот файл +├── TASKS.md # Пошаговый план разработки +├── AGENTS.md # Стратегия выбора AI-моделей +├── .cursorrules # Правила для AI-ассистента в IDE +│ +├── frontend/ # Next.js приложение +│ ├── src/ +│ │ ├── app/ # Next.js App Router (pages, layout) +│ │ ├── components/ # UI компоненты +│ │ │ ├── editor/ # Tiptap редактор, Toolbar, AI-меню +│ │ │ ├── sidebar/ # FileExplorer, StoryExplorer +│ │ │ └── ui/ # Переиспользуемые кнопки, инпуты +│ │ ├── lib/ # Утилиты, конфигурация axios, типы +│ │ └── stores/ # Zustand stores (appStore, editorStore) +│ └── package.json +│ +└── backend/ # FastAPI сервер + ├── main.py # Точка входа, настройка CORS + ├── routers/ # API эндпоинты (projects, documents, ai) + ├── services/ # Бизнес-логика (file_ops, llm_router) + └── models/ # Pydantic схемы запросов/ответов \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..23febd4 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# Writer IDE + +Fullstack IDE для написания и редактирования текстов с интеграцией AI. + +## 📁 Структура проекта + +``` +writer-ide/ +├── frontend/ # Next.js приложение +├── backend/ # FastAPI сервер +├── .gitea/workflows/ # CI/CD pipelines +└── docker-compose.yml # Оркестрация контейнеров +``` + +## 🚀 CI/CD и Деплой + +Проект настроен на автоматическую сборку и публикацию Docker-образов через **Gitea Actions**. + +### Как это работает: + +1. При пуше в ветку `main`/`master` или создании тега (например, `v1.0.0`) запускается workflow `.gitea/workflows/ci.yml`. +2. Собираются образы `backend` и `frontend` с использованием Docker Buildx и кэширования. +3. Образы публикуются во встроенный **Gitea Container Registry**. +4. (Опционально) Workflow `deploy.yml` автоматически подключается к серверу по SSH, стягивает новые образы и перезапускает сервисы через `docker-compose`. + +### Настройка секретов в репозитории Gitea: + +Перейдите в **Settings → Secrets and variables → Actions** и добавьте: + +- `GITEA_TOKEN` — токен с доступом к Container Registry (используется в ci.yml) +- `DEPLOY_HOST` — IP или домен вашего сервера +- `DEPLOY_USER` — Имя пользователя для SSH +- `DEPLOY_SSH_KEY` — Приватный SSH-ключ (без парольной фразы) для подключения к серверу + +### Включение Actions в репозитории: + +Убедитесь, что в настройках репозитория (**Settings → Repository**) включена опция **Enable Actions**. + +## 🛠 Разработка + +### Установка зависимостей + +#### Frontend (Next.js) +```bash +cd frontend +npm install +``` + +#### Backend (FastAPI) +```bash +cd backend +pip install -r requirements.txt +``` + +### Запуск в режиме разработки + +#### Frontend +```bash +cd frontend +npm run dev +``` + +#### Backend +```bash +cd backend +uvicorn main:app --reload +``` + +## 🐳 Docker + +### Сборка образов +```bash +docker-compose build +``` + +### Запуск +```bash +docker-compose up +``` + +### Остановка +```bash +docker-compose down +``` + +## 📄 Лицензия + +MIT diff --git a/STATUS.md b/STATUS.md new file mode 100644 index 0000000..aa771d9 --- /dev/null +++ b/STATUS.md @@ -0,0 +1,144 @@ +📊 Writer IDE — Summary проекта +🎯 Видение проекта +Локально-ориентированное веб-приложение (IDE для писателей), объединяющее rich-text редактирование, структурированное управление сюжетом и бесшовную интеграцию AI-ассистента для генерации, редактуры и анализа текста. +🛠️ Технологический стек + + Frontend: Next.js 14+ (App Router), TypeScript, Tailwind CSS, Tiptap (редактор), Zustand (state management) + Backend: FastAPI (Python), Pydantic, Uvicorn + AI Интеграция: Ollama (локально), OpenRouter API (облачные модели) + Разработка: Zed IDE + AI-агенты (Qwen 2.5 Coder 7b/32b через OpenRouter) + +✅ Что уже сделано (Этап 1: Фундамент) +Задача + +Статус + +Что реализовано +1.1 Инициализация Frontend + +✅ Выполнено + +Next.js + Tailwind + Tiptap + Zustand установлены +1.2 Zustand Store + +✅ Выполнено + +appStore.ts с управлением документами (id, title, content, type) +1.3 Layout (3 колонки) + +✅ Выполнено + +FileExplorer (слева), StoryExplorer (посередине), Editor (справа) +1.4 Tiptap Editor + +✅ Выполнено + +Rich-text редактор с расширениями: TextStyle, Color, Underline, Blockquote, интеграция с Zustand +1.5 Toolbar + +✅ Выполнено + +Панель форматирования: Bold, Italic, Underline, Quote, Color picker, Clear formatting, Font family, Font size +1.6 Backend инициализация + +✅ Выполнено + +FastAPI с CORS, health check endpoint, структура папок (routers, services, models) +1.7 Frontend-Backend интеграция + +✅ Выполнено + +API endpoints для документов, автосохранение с debounce, персистентность данных +Итог Этапа 1: Рабочее приложение с rich-text редактором, трехколоночным интерфейсом и синхронизацией с backend. +🟡 Что планируется (Этап 2: AI Интеграция) +Задача + +Статус + +Что будет реализовано +2.1 Backend AI сервис + +⏳ Готово к началу + +LLMRouter для Ollama и OpenRouter, эндпоинт /api/ai/generate +2.2 Система пресетов + +⏳ Ожидает + +Пресеты: Creative Draft (temp 0.85), Editor (temp 0.3), Brainstorm (temp 1.0) +2.3 Frontend AI клиент + +⏳ Ожидает + +Типизированный API клиент ai-api.ts для вызова генерации +2.4 Inline AI меню + +⏳ Ожидает + +Всплывающее меню при выделении текста с действиями: Переписать, Расширить, Упростить +2.5 Diff view + +⏳ Ожидает + +Визуальное сравнение оригинального текста и AI-предложения с кнопками Accept/Reject +2.6 Интеграция AI действий + +⏳ Ожидает + +Связывание AIMenu + AIDiffView + API для полноценного AI-редактирования +Цель Этапа 2: Пользователь может выделить текст, выбрать AI действие, увидеть предпросмотр изменений в diff view и принять/отклонить. +🟠 Что планируется (Этап 3: Контекст и Story Bible) + + Автоматическое извлечение сущностей (персонажи, локации) из текста + Панель Story Bible для управления контекстом + Умная подача контекста в промпты для консистентности повествования + Фоновый агент для обновления Story Bible при сохранении глав + +🔴 Что планируется (Этап 4: Экспорт) + + Экспорт в PDF с форматированием (шрифты, отступы, оглавление) + Экспорт в EPUB/FB2 для читалок + Генерация иллюстраций (опционально) + +🎯 Ключевые архитектурные решения + + Local First: Данные пользователя хранятся локально (backend как прокси к файловой системе) + AI Router: Абстракция над Ollama и OpenRouter для гибкого переключения между локальными и облачными моделями + Presets System: Жесткое разделение параметров генерации в зависимости от задачи (творчество vs анализ) + Inline AI: AI-действия не прерывают поток писателя (встроенные меню, diff view вместо чата) + Type Safety: Строгая типизация на frontend (TypeScript) и backend (Pydantic) + +📁 Структура проекта + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 + +🚀 Следующий шаг (завтра) +Задача 2.1: Backend AI сервис + + Модель: qwen/qwen-2.5-coder-32b-instruct (через OpenRouter) + Что сделать: Создать backend/services/llm_router.py с поддержкой Ollama и OpenRouter + Критерий успеха: Эндпоинт POST /api/ai/generate принимает промпт и возвращает сгенерированный текст + +💡 Важные заметки + + Python 3.14 на сервере — используем пакеты без жестких версий в requirements.txt + Zed IDE вместо VS Code для экономии памяти (16 GB RAM) + Для разработки используем OpenRouter (Qwen 2.5 Coder), для тестирования функционала — локальную Ollama diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..d643350 --- /dev/null +++ b/TASKS.md @@ -0,0 +1,258 @@ + +--- + +### 📄 Файл: `TASKS.md` (Этап 1: Полный разбор) + +```markdown +# Задачи проекта Writer IDE + +## 🟢 Этап 1: Фундамент, Layout и Базовый Редактор + +### Задача 1.1: Инициализация Frontend и установка зависимостей +**Описание**: Создание базового Next.js приложения и установка всех необходимых библиотек для Этапа 1. +**Критерии успеха**: +- Команда `npm run dev` в папке `frontend` запускает приложение на `localhost:3000`. +- Tailwind CSS работает (проверка через класс `bg-red-500`). +**Инструкция для AI-агента**: +> "Инициализируй Next.js проект в текущей папке с TypeScript, Tailwind, App Router и src-директорией. Затем установи следующие зависимости: `@tiptap/react`, `@tiptap/starter-kit`, `@tiptap/extension-text-style`, `@tiptap/extension-color`, `@tiptap/extension-blockquote`, `@tiptap/extension-underline`, `zustand`, `lucide-react`, `axios`. Убедись, что `tailwind.config.ts` настроен корректно." + +--- + +### Задача 1.2: Настройка Zustand Store (Состояние приложения) +**Описание**: Создание глобального хранилища для управления активным документом, его содержимым и состоянием боковых панелей. +**Критерии успеха**: +- Создан файл `frontend/src/stores/appStore.ts`. +- Store содержит: `activeDocumentId`, `documents` (массив объектов с id, title, content), функции `setActiveDocument`, `updateDocumentContent`. +**Инструкция для AI-агента**: +> "Создай Zustand store в `src/stores/appStore.ts`. Определи интерфейс `Document` (id: string, title: string, content: string, type: 'scene' | 'chapter'). Добавь состояния для списка документов и активного документа, а также экшены для их обновления. Инициализируй store одним тестовым документом." + +--- + +### Задача 1.3: Архитектура Layout (Трехколоночный интерфейс) +**Описание**: Создание основного макета приложения `src/app/layout.tsx` и `src/app/page.tsx` с тремя колонками. +**Критерии успеха**: +- Левая колонка (w-64): Заглушка компонента `FileExplorer`. +- Средняя колонка (w-72): Заглушка компонента `StoryExplorer`. +- Правая колонка (flex-1): Область для редактора. +- Используется CSS Grid или Flexbox. Адаптивность: на экранах < 1024px боковые панели скрываются (или сжимаются). +**Инструкция для AI-агента**: +> "Создай компоненты-заглушки `FileExplorer.tsx` и `StoryExplorer.tsx` в `src/components/sidebar/`. Реализуй основной Layout в `src/app/page.tsx`, используя Flexbox или Grid для создания трех колонок с фиксированной шириной для сайдбаров и `flex-1` для центральной области. Примени темную/светлую тему через Tailwind." + +--- + +### Задача 1.4: Ядро Tiptap Редактора +**Описание**: Создание переиспользуемого компонента редактора с поддержкой базового форматирования. +**Критерии успеха**: +- Компонент `src/components/editor/Editor.tsx` инициализирует `useEditor` с расширениями: `StarterKit`, `TextStyle`, `Color`, `Underline`, `Blockquote`. +- Редактор привязан к Zustand store (загружает `content` активного документа и сохраняет изменения через `onUpdate`). +**Инструкция для AI-агента**: +> "Создай компонент `Editor.tsx`. Используй `@tiptap/react`. Подключи расширения: StarterKit, TextStyle, Color, Underline, Blockquote. Настрой `onUpdate`, чтобы при изменении текста вызывался экшен `updateDocumentContent` из `appStore`. Оберни редактор в div с классами для красивого отображения (max-w-3xl, mx-auto, prose)." + +--- + +### Задача 1.5: Панель инструментов (Toolbar) для форматирования +**Описание**: Создание плавающей или верхней панели инструментов для управления стилями текста. +**Критерии успеха**: +- Компонент `src/components/editor/Toolbar.tsx`. +- Кнопки для: Bold, Italic, Underline, Blockquote, выбор цвета текста (используй стандартный `` для простоты). +- Кнопки используют `editor.chain().focus().toggleBold().run()` и аналоги. +**Инструкция для AI-агента**: +> "Создай компонент `Toolbar.tsx`, который принимает экземпляр `editor` из Tiptap как пропс. Реализуй кнопки форматирования с использованием Lucide React иконок. Добавь простую логику подсветки активной кнопки (через `editor.isActive('bold')`). Размести этот Toolbar над компонентом Editor." + +--- + +### Задача 1.6: Инициализация Backend (FastAPI) +**Описание**: Базовая настройка Python-сервера для обслуживания frontend и подготовки к работе с файлами. +**Критерии успеха**: +- Файл `backend/main.py` запускается через `uvicorn main:app --reload`. +- Настроен CORS middleware, разрешающий запросы с `http://localhost:3000`. +- Эндпоинт `GET /api/health` возвращает `{"status": "ok"}`. +**Инструкция для AI-агента**: +> "Создай базовое приложение FastAPI в `backend/main.py`. Добавь `CORSMiddleware` с разрешенными источниками `http://localhost:3000`. Создай простой роутер `/api/health`. Напиши инструкцию по запуску (создание venv, `pip install fastapi uvicorn pydantic`, запуск)." + +--- + +### Задача 1.7: Интеграция Frontend и Backend (Сохранение/Загрузка) +**Описание**: Связывание Zustand store с Backend для имитации сохранения данных (пока в памяти или простом JSON). +**Критерии успеха**: +- При изменении текста в редакторе, через debounced-запрос (или по кнопке "Сохранить") данные отправляются на backend. +- При загрузке страницы frontend запрашивает список документов и контент активного документа с backend. +**Инструкция для AI-агента**: +> "Создай в `backend` простой in-memory список документов (или запись в `data.json`). Добавь эндпоинты `GET /api/documents` и `POST /api/documents/{id}/content`. В `frontend/src/lib/api.ts` создай типизированные функции для этих запросов. Обнови `appStore`, чтобы он загружал данные при инициализации." + +--- + +## 🟡 Этап 2: AI Интеграция и Inline-действия + +### Задача 2.1: Backend AI сервис (базовый роутер для LLM) +**Описание**: Создание сервиса на backend для работы с AI-моделями (Ollama локально и OpenRouter API). +**Критерии успеха**: +- Создан файл `backend/services/llm_router.py` +- Сервис поддерживает два провайдера: `ollama` и `openrouter` +- Эндпоинт `POST /api/ai/generate` принимает промпт и возвращает сгенерированный текст +- Поддержка выбора модели через параметр запроса +**Инструкция для AI-агента**: +> "Создай сервис `backend/services/llm_router.py` с классом `LLMRouter`. Реализуй методы: +> - `generate_ollama(prompt, model)` — вызов локальной Ollama через `http://localhost:11434/api/generate` +> - `generate_openrouter(prompt, model, api_key)` — вызов OpenRouter API +> - `generate(prompt, provider, model, **kwargs)` — универсальный метод, который маршрутизирует запрос +> Создай эндпоинт `POST /api/ai/generate` в `backend/routers/ai.py`, который принимает JSON: `{prompt, provider, model, options}` и возвращает `{generated_text}`. Используй `httpx` для async HTTP запросов. Добавь `httpx` в requirements.txt." + +--- + +### Задача 2.2: Система пресетов для генерации +**Описание**: Реализация пресетов параметров генерации (Creative Draft, Editor, Brainstorm) на backend. +**Критерии успеха**: +- Создан файл `backend/services/presets.py` +- Определены три пресета с разными параметрами (temperature, top_p, frequency_penalty, presence_penalty) +- Эндпоинт `/api/ai/generate` принимает параметр `preset` и применяет соответствующие настройки +**Инструкция для AI-агента**: +> "Создай `backend/services/presets.py` с тремя пресетами: +> - `CREATIVE_DRAFT`: temperature=0.85, top_p=0.92, frequency_penalty=0.3, presence_penalty=0.4 +> - `EDITOR`: temperature=0.3, top_p=0.85, frequency_penalty=0.0, presence_penalty=0.0 +> - `BRAINSTORM`: temperature=1.0, top_p=0.95, frequency_penalty=0.5, presence_penalty=0.6 +> Обнови `llm_router.py`, чтобы метод `generate` принимал параметр `preset` и применял соответствующие параметры к запросу. Обнови эндпоинт `/api/ai/generate` для поддержки пресетов." + +--- + +### Задача 2.3: Frontend API клиент для AI +**Описание**: Создание типизированного API клиента на frontend для работы с AI эндпоинтами. +**Критерии успеха**: +- Создан файл `frontend/src/lib/ai-api.ts` +- Функция `generateText(prompt, provider, model, preset)` возвращает сгенерированный текст +- Все типы типизированы (никаких `any`) +**Инструкция для AI-агента**: +> "Создай `frontend/src/lib/ai-api.ts` с типами: +> ```typescript +> interface GenerateRequest { +> prompt: string; +> provider: 'ollama' | 'openrouter'; +> model: string; +> preset: 'creative_draft' | 'editor' | 'brainstorm'; +> options?: Record; +> } +> interface GenerateResponse { +> generated_text: string; +> } +> ``` +> Реализуй функцию `generateText(request: GenerateRequest): Promise`, которая вызывает `POST /api/ai/generate` и возвращает `generated_text`. Используй axios из `lib/api.ts`." + +--- + +### Задача 2.4: Inline AI меню в редакторе (UI) +**Описание**: Создание всплывающего меню при выделении текста в редакторе для выбора AI действий. +**Критерии успеха**: +- Создан компонент `frontend/src/components/editor/AIMenu.tsx` +- При выделении текста появляется кнопка "✨ AI" +- При клике на кнопку открывается меню с действиями: "Переписать", "Расширить", "Упростить", "Свой промпт" +**Инструкция для AI-агента**: +> "Создай компонент `AIMenu.tsx`, который использует Tiptap BubbleMenu или FloatingMenu. При выделении текста показывай кнопку с иконкой `Sparkles` из lucide-react. При клике на кнопку открывай dropdown с пунктами: 'Переписать', 'Расширить', 'Упростить', 'Свой промпт...'. Каждый пункт при клике вызывает callback `onAction(action: string, selectedText: string)`. Стилизуй меню в темной теме (bg-gray-800, border-gray-700)." + +--- + +### Задача 2.5: Diff view для предпросмотра AI изменений +**Описание**: Создание компонента для визуального сравнения оригинального текста и AI-предложения. +**Критерии успеха**: +- Создан компонент `frontend/src/components/editor/AIDiffView.tsx` +- Компонент показывает оригинальный текст слева и AI-текст справа +- Есть кнопки "Принять" и "Отклонить" +- Использует библиотеку для сравнения текста (например, `diff` или простую визуализацию) +**Инструкция для AI-агента**: +> "Создай компонент `AIDiffView.tsx`, который принимает пропсы: `originalText: string`, `aiText: string`, `onAccept: () => void`, `onReject: () => void`. Отображай два блока текста (можно просто два div рядом или друг под другом). Добавь кнопки '✅ Принять' и '❌ Отклонить' вверху. Стилизуй: оригинальный текст с красноватым фоном (bg-red-900/20), AI текст с зеленоватым (bg-green-900/20). Кнопки: Accept — bg-green-600, Reject — bg-red-600." + +--- + +### Задача 2.6: Интеграция AI действий с редактором +**Описание**: Связывание AIMenu, AIDiffView и API клиента для полноценной работы AI-редактирования. +**Критерии успеха**: +- При выборе действия в AIMenu вызывается API с соответствующим промптом +- Пока идет генерация, показывается спиннер +- После получения ответа открывается AIDiffView +- При нажатии "Принять" выделенный текст заменяется на AI-текст +- При нажатии "Отклонить" возвращается оригинальный текст +**Инструкция для AI-агента**: +> "Интегрируй AIMenu и AIDiffView в Editor.tsx: +> 1. Добавь состояние: `selectedText`, `aiGeneratedText`, `showDiffView`, `isLoading` +> 2. При выборе действия в AIMenu: +> - Сохрани `selectedText` из `editor.state.doc.textBetween(selection.from, selection.to)` +> - Сформируй промпт в зависимости от действия (например, для 'Переписать': 'Перепиши этот текст, сделай его более живым и кинематографичным: [selectedText]') +> - Вызови `generateText` из `ai-api.ts` с пресетом `creative_draft` +> - Установи `aiGeneratedText` и `showDiffView = true` +> 3. В AIDiffView: +> - `onAccept`: замени выделенный текст через `editor.chain().focus().deleteRange(selection).insertContent(aiGeneratedText).run()` +> - `onReject`: закрой diff view, ничего не меняй +> 4. Покажи спиннер (из lucide-react: `Loader2` с анимацией `animate-spin`), пока `isLoading = true`" + +--- + +## 🟠 Этап 3: Контекст и Story Bible + +### Задача 3.1: Backend модель и API для Story Bible +**Описание**: Создание структуры данных для Story Bible (персонажи, локации, события) и API для работы с ней. +**Критерии успеха**: +- Созданы Pydantic модели для сущностей (Character, Location, Event, StoryThread) +- Создан эндпоинт `GET /api/projects/{id}/bible` для получения Story Bible +- Создан эндпоинт `PUT /api/projects/{id}/bible` для обновления Story Bible +- Данные хранятся в JSON файле на диске (локально) +**Рекомендуемая модель**: `qwen/qwen-2.5-coder-7b-instruct` (простая CRUD задача) +**Инструкция для AI-агента**: +> "Создай Pydantic модели в `backend/models/bible.py` для сущностей: Character (id, name, description, traits, appearance), Location (id, name, description, atmosphere), Event (id, title, description, chapter), StoryThread (id, title, description, status). Создай модель StoryBible, которая содержит списки этих сущностей. Создай роутер `backend/routers/bible.py` с эндпоинтами для получения и обновления Story Bible. Данные храни в файле `data/story_bible.json`. Добавь функцию инициализации с примером данных." + +--- + +### Задача 3.2: Frontend UI для Story Bible (панель) +**Описание**: Создание панели для просмотра и редактирования Story Bible. +**Критерии успеха**: +- Создан компонент `StoryBiblePanel.tsx` с вкладками: Персонажи, Локации, События, Сюжетные нити +- Каждая вкладка показывает список сущностей с возможностью редактирования +- Есть кнопки "Добавить" и "Удалить" для каждой сущности +- Панель интегрирована в layout (заменяет или дополняет StoryExplorer) +**Рекомендуемая модель**: `qwen/qwen-2.5-coder-7b-instruct` (UI компонент) +**Инструкция для AI-агента**: +> "Создай компонент `src/components/sidebar/StoryBiblePanel.tsx` с вкладками для персонажей, локаций, событий и сюжетных нитей. Каждая вкладка показывает список карточек сущностей. Карточка содержит название, краткое описание и кнопки редактирования/удаления. Добавь модальное окно для создания/редактирования сущности с полями, соответствующими модели. Интегрируй панель в layout, заменив заглушку StoryExplorer. Используй Zustand store для управления состоянием." + +--- + +### Задача 3.3: Настройки выбора моделей для разных задач +**Описание**: Создание UI и backend логики для выбора моделей под разные задачи (генерация, фоновая обработка, анализ). +**Критерии успеха**: +- Создан компонент `ModelSettings.tsx` с выбором моделей для: + - Генерации текста (основная модель) + - Фоновой обработки (извлечение сущностей) + - Анализа текста (проверка логики) +- Настройки сохраняются в localStorage или backend +- При генерации используются выбранные модели +**Рекомендуемая модель**: `qwen/qwen-2.5-coder-7b-instruct` (UI + интеграция) +**Инструкция для AI-агента**: +> "Создай компонент `src/components/settings/ModelSettings.tsx` с dropdown для выбора моделей для разных задач: 'Основная модель для генерации', 'Модель для фоновой обработки', 'Модель для анализа'. Загружай список доступных моделей через `getAvailableModels()` из `ai-api.ts`. Сохраняй настройки в Zustand store. Обнови `ai-api.ts`, чтобы функция `generateText` принимала параметр `task_type` и использовала соответствующую модель из настроек." + +--- + +### Задача 3.4: Фоновый агент для извлечения сущностей +**Описание**: Создание сервиса для автоматического извлечения персонажей, локаций и событий из текста главы. +**Критерии успеха**: +- Создан сервис `backend/services/entity_extractor.py` +- Сервис принимает текст главы и возвращает JSON с извлеченными сущностями +- Использует промпт для структурированного вывода (JSON mode) +- Интегрирован с Story Bible (обновляет или добавляет новые сущности) +**Рекомендуемая модель**: `qwen/qwen-2.5-coder-32b-instruct` (сложный промпт-инжиниринг) +**Инструкция для AI-агента**: +> "Создай сервис `backend/services/entity_extractor.py` с функцией `extract_entities(text, existing_bible)`. Функция формирует промпт для LLM, который извлекает из текста новых персонажей, локации и события в формате JSON. Промпт должен включать примеры формата вывода и инструкцию не дублировать существующие сущности из `existing_bible`. Используй модель для фоновой обработки из настроек. Пресет: `editor` (низкая температура для точности)." + +--- + +### Задача 3.5: Автоматическое обновление Story Bible при сохранении +**Описание**: Интеграция извлечения сущностей с процессом сохранения главы. +**Критерии успеха**: +- При сохранении главы (автосохранение) вызывается фоновый агент +- Агент анализирует текст и обновляет Story Bible +- Пользователь видит уведомление "Story Bible обновлена" +- Есть возможность отключить автоматическое обновление в настройках +**Рекомендуемая модель**: `qwen/qwen-2.5-coder-32b-instruct` (интеграция) +**Инструкция для AI-агента**: +> "Обнови `Editor.tsx`, чтобы при автосохранении (после debounce) отправлял запрос на backend для извлечения сущностей. Создай эндпоинт `POST /api/ai/extract-entities` в `backend/routers/ai.py`, который принимает текст главы и текущую Story Bible, вызывает `entity_extractor`, и возвращает обновленную Bible. На frontend добавь переключатель 'Автоматически обновлять Story Bible' в настройках. Показывай уведомление (toast) при успешном обновлении." + +--- + +## 🔴 Этап 4: Экспорт и полировка +*(Будет активирован после успешного завершения Этапа 3)* diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..49a36fe --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,40 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ +.venv + +# Данные (монтируются как volume, не копируются в образ) +data/ + +# Переменные окружения (секреты не должны попасть в образ) +.env + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Git +.git/ +.gitignore + +# Docker +Dockerfile +docker-compose*.yml +.dockerignore + +# Документация +*.md +!requirements.txt + +# Тесты +tests/ +.pytest_cache/ +.coverage diff --git a/backend/.env.docker b/backend/.env.docker new file mode 100644 index 0000000..42d13ca --- /dev/null +++ b/backend/.env.docker @@ -0,0 +1,18 @@ +# OpenRouter API +OPENROUTER_API_KEY=your_api_key_here +OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 + +# Ollama — имя контейнера в docker-compose сети +OLLAMA_BASE_URL=http://ollama:11434 + +# Путь к данным — монтируется volume +DATA_DIR=/app/data + +# Окружение +ENVIRONMENT=production + +# CORS — домен вашего сервера +CORS_ORIGINS=http://localhost:3000,http://your-server-ip:3000 + +# JWT секрет — сгенерируйте случайную строку! +JWT_SECRET_KEY=generate-a-long-random-string-here diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..16c4b61 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,18 @@ +# OpenRouter API +OPENROUTER_API_KEY=your_api_key_here +OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 + +# Ollama — в Docker это будет http://ollama:11434 или IP хоста +OLLAMA_BASE_URL=http://localhost:11434 + +# Путь к данным — в Docker будет монтироваться volume +DATA_DIR=/app/data + +# Окружение +ENVIRONMENT=development + +# CORS — через запятую, если несколько источников +CORS_ORIGINS=http://localhost:3000,http://your-domain.com + +# JWT секрет — ОБЯЗАТЕЛЬНО измените в production! +JWT_SECRET_KEY=change-this-to-a-random-secret-string diff --git a/backend/.env.local b/backend/.env.local new file mode 100644 index 0000000..7b5da70 --- /dev/null +++ b/backend/.env.local @@ -0,0 +1,8 @@ +# OpenRouter API Key (получить на https://openrouter.ai) +OPENROUTER_API_KEY="sk-yapWeqHgWRbBQZcWOKSlq4bmd_uYC2oB" + +# Ollama URL (по умолчанию localhost) +OLLAMA_BASE_URL=http://localhost:11434 + +# Режим разработки +ENVIRONMENT=development diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..18b86cb --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,11 @@ +venv/ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.so +.coverage +.pytest_cache/ +data/ +.env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..89a3cc4 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,49 @@ +# Используем slim-образ Python для меньшего размера +FROM python:3.11-slim + +# Устанавливаем системные зависимости для WeasyPrint и Pandoc +# WeasyPrint требует: cairo, pango, gdk-pixbuf2, libffi +# Pandoc — просто устанавливается через apt +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Для WeasyPrint + libpango-1.0-0 \ + libpangoft2-1.0-0 \ + libcairo2 \ + libgdk-pixbuf2.0-0 \ + libffi-dev \ + shared-mime-info \ + # Для Pandoc + pandoc \ + # Утилиты + curl \ + && rm -rf /var/lib/apt/lists/* + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем requirements и устанавливаем Python-зависимости +# (отдельным слоем для кэширования) +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Копируем исходный код +COPY . . + +# Создаём папку для данных (будет переопределена volume при запуске) +RUN mkdir -p /app/data + +# Обновляем кэш MIME-типов (нужно для WeasyPrint) +RUN update-mime-database /usr/share/mime + +# Переменные окружения по умолчанию +ENV PYTHONUNBUFFERED=1 \ + DATA_DIR=/app/data \ + OLLAMA_BASE_URL=http://ollama:11434 \ + ENVIRONMENT=production + +# Открываем порт +EXPOSE 8000 + +# Команда запуска +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..0b80174 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,91 @@ +# Writer IDE Backend + +## Установка + +```bash +cd backend +python3 -m venv venv +source venv/bin/activate # На Windows: venv\Scripts\activate +pip install -r requirements.txt +``` + +## Переменные окружения + +Все настройки читаются из переменных окружения. Создайте файл `.env` на основе `.env.example`: + +```bash +cp .env.example .env +# Отредактируйте .env, указав ваши значения +``` + +### Ключевые переменные + +| Переменная | Описание | По умолчанию | +|------------|----------|--------------| +| `OPENROUTER_API_KEY` | API ключ OpenRouter | (пусто) | +| `OPENROUTER_BASE_URL` | URL OpenRouter API | https://openrouter.ai/api/v1 | +| `OLLAMA_BASE_URL` | URL Ollama сервера | http://localhost:11434 | +| `DATA_DIR` | Путь к папке с данными | data | +| `CORS_ORIGINS` | Разрешённые источники (через запятую) | http://localhost:3000 | +| `JWT_SECRET_KEY` | Секрет для JWT токенов | (захардкожен, изменить в production!) | +| `ENVIRONMENT` | Окружение | development | + +### Docker-окружение + +При запуске в Docker используйте файл `.env.docker` как шаблон: + +```bash +cp .env.docker .env +``` + +Обратите внимание: +- `OLLAMA_BASE_URL` должен указывать на контейнер Ollama (например, http://ollama:11434) +- `DATA_DIR` должен быть `/app/data` (монтируется volume) +- `CORS_ORIGINS` должен включать домен/IP вашего сервера +- `JWT_SECRET_KEY` **обязательно** измените на случайную строку! + +## Запуск + +```bash +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +## Проверка +- API Health: http://localhost:8000/api/health +- Swagger UI: http://localhost:8000/docs + +## Если возникли проблемы с Python 3.14 + +Если пакеты не устанавливаются, используйте pyenv для создания окружения с Python 3.12: + +```bash +pyenv install 3.12.0 +pyenv local 3.12.0 +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +## Установка Pandoc (для экспорта в EPUB, FB2, PDF, DOCX) + +Для работы функции экспорта необходимо установить [Pandoc](https://pandoc.org/) — универсальный конвертер документов. + +### Ubuntu / Debian +```bash +sudo apt-get install pandoc +``` + +### macOS +```bash +brew install pandoc +``` + +### Windows +Скачайте установщик с официального сайта: [pandoc.org/installing.html](https://pandoc.org/installing.html) + +### Проверка установки +```bash +pandoc --version +``` + +> **Примечание:** Для экспорта в PDF также требуется LaTeX-движок (например, `texlive-xetex` на Ubuntu или `MacTeX` на macOS). По умолчанию используется `xelatex`. diff --git a/backend/config/models.json b/backend/config/models.json new file mode 100644 index 0000000..31316d3 --- /dev/null +++ b/backend/config/models.json @@ -0,0 +1,72 @@ +{ + "ollama": { + "default_model": "qwen2.5:1.5b", + "available_models": [ + { + "id": "qwen2.5:1.5b", + "name": "Qwen 2.5 1.5B (Local)", + "description": "Компактная модель для быстрых задач", + "context_length": 32768 + }, + { + "id": "qwen2.5:7b", + "name": "Qwen 2.5 7B (Local)", + "description": "Баланс скорости и качества", + "context_length": 32768 + }, + { + "id": "qwen2.5:14b", + "name": "Qwen 2.5 14B (Local)", + "description": "Высокое качество, требует больше ресурсов", + "context_length": 32768 + } + ] + }, + "openrouter": { + "default_model": "qwen/qwen-2.5-coder-7b-instruct", + "available_models": [ + { + "id": "qwen/qwen-2.5-coder-7b-instruct", + "name": "Qwen 2.5 Coder 7B", + "description": "Специализированная модель для кода, быстрая и дешевая", + "context_length": 32768, + "pricing": "$0.0001/1K tokens" + }, + { + "id": "qwen/qwen-2.5-coder-32b-instruct", + "name": "Qwen 2.5 Coder 32B", + "description": "Мощная модель для сложных задач", + "context_length": 32768, + "pricing": "$0.0008/1K tokens" + }, + { + "id": "qwen/qwen-2.5-7b-instruct", + "name": "Qwen 2.5 7B Instruct", + "description": "Универсальная модель для текста", + "context_length": 32768, + "pricing": "$0.0001/1K tokens" + }, + { + "id": "qwen/qwen-2.5-72b-instruct", + "name": "Qwen 2.5 72B Instruct", + "description": "Максимальное качество, дороже", + "context_length": 32768, + "pricing": "$0.0008/1K tokens" + }, + { + "id": "meta-llama/llama-3.1-8b-instruct", + "name": "Llama 3.1 8B", + "description": "Универсальная модель Meta", + "context_length": 131072, + "pricing": "$0.0001/1K tokens" + }, + { + "id": "google/gemma-2-9b-it", + "name": "Gemma 2 9B", + "description": "Хороша для креативных задач", + "context_length": 8192, + "pricing": "$0.0001/1K tokens" + } + ] + } +} diff --git a/backend/config/settings.py b/backend/config/settings.py new file mode 100644 index 0000000..4b0368d --- /dev/null +++ b/backend/config/settings.py @@ -0,0 +1,48 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + +# Загружаем переменные окружения из .env файла +load_dotenv() + + +class Settings: + # API ключи + OPENROUTER_API_KEY: str = os.getenv("OPENROUTER_API_KEY", "") + OPENROUTER_BASE_URL: str = os.getenv( + "OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1" + ) + + # Ollama — в Docker это будет не localhost, а имя контейнера или IP хоста + OLLAMA_BASE_URL: str = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + + # Путь к данным — по умолчанию локальная папка, в Docker будет монтироваться volume + DATA_DIR: str = os.getenv("DATA_DIR", "data") + + # Окружение + ENVIRONMENT: str = os.getenv("ENVIRONMENT", "development") + + # CORS — разрешённые источники (в production это будет домен) + CORS_ORIGINS: list = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",") + + # JWT секрет — в production обязательно переопределить через .env + JWT_SECRET_KEY: str = os.getenv( + "JWT_SECRET_KEY", "your-super-secret-key-change-in-production" + ) + + def validate(self) -> dict: + """Проверить, какие провайдеры настроены""" + return { + "ollama": True, # Ollama всегда доступна локально + "openrouter": bool( + self.OPENROUTER_API_KEY + and self.OPENROUTER_API_KEY != "your_api_key_here" + ), + "data_dir": self.DATA_DIR, + "cors_origins": self.CORS_ORIGINS, + } + + +# Singleton instance +settings = Settings() diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..dd9fd8d --- /dev/null +++ b/backend/main.py @@ -0,0 +1,44 @@ +from config.settings import settings +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from routers import ai, auth, bible, documents, export, health, images, projects +from services.document_service import document_service +from services.project_service import project_service + +app = FastAPI(title="Writer IDE API", version="0.1.0") + +# CORS configuration - uses settings from .env +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include routers +app.include_router(health.router, prefix="/api") +app.include_router(auth.router, prefix="/api") +app.include_router(projects.router, prefix="/api") +app.include_router(documents.router, prefix="/api") +app.include_router(ai.router, prefix="/api") +app.include_router(bible.router, prefix="/api") +app.include_router(export.router, prefix="/api") +app.include_router(images.router, prefix="/api") + + +@app.on_event("startup") +async def startup_event(): + """Инициализация тестового документа в активном проекте при запуске сервера""" + # Если есть проекты, инициализируем тестовый документ в первом + all_projects = project_service.get_all_projects() + if all_projects: + default_project = all_projects[0] + document_service.initialize_test_document( + default_project.id, default_project.user_id + ) + + +@app.get("/") +async def root(): + return {"message": "Writer IDE API", "docs": "/docs"} diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/models/ai.py b/backend/models/ai.py new file mode 100644 index 0000000..c64ec72 --- /dev/null +++ b/backend/models/ai.py @@ -0,0 +1,95 @@ +from typing import Any, Dict, List, Literal, Optional + +from pydantic import BaseModel + +from models.bible import StoryBible + + +class GenerateRequest(BaseModel): + prompt: str + provider: Literal["ollama", "openrouter"] = "ollama" + model: Optional[str] = None + api_key: Optional[str] = None # Опционально, обычно берется из .env + preset: Literal["creative_draft", "editor", "brainstorm"] = "creative_draft" + options: Optional[Dict[str, Any]] = None + story_bible: Optional[StoryBible] = None # Story Bible для контекста + selected_text: Optional[str] = None # Текст, который будет обрабатываться + + +class GenerateResponse(BaseModel): + generated_text: str + provider: str + model: str + preset: str + + +class ExtractedEntities(BaseModel): + new_characters: List[Dict[str, Any]] = [] + new_locations: List[Dict[str, Any]] = [] + new_events: List[Dict[str, Any]] = [] + updated_traits: List[Dict[str, Any]] = [] + + +class ExtractEntitiesRequest(BaseModel): + text: str + existing_bible: Optional[StoryBible] = None + task_type: str = "background" + + +class ExtractEntitiesResponse(BaseModel): + entities: ExtractedEntities + model_used: str + provider: str + + +class AutoUpdateBibleRequest(BaseModel): + project_id: str + scene_text: str + auto_update_enabled: bool = True + + +class AutoUpdateBibleResponse(BaseModel): + updated: bool + new_characters: int = 0 + new_locations: int = 0 + new_events: int = 0 + message: str = "" + + +class CleanupDuplicatesResponse(BaseModel): + removed_characters: int = 0 + removed_locations: int = 0 + removed_events: int = 0 + message: str = "" + + +class PreviewContextRequest(BaseModel): + text: str + story_bible: StoryBible + + +class PreviewContextResponse(BaseModel): + context_string: str + mentioned_characters: List[str] + mentioned_locations: List[str] + mentioned_events: List[str] + + +class GenerateImageRequest(BaseModel): + prompt: str + project_id: str + document_id: str # ID документа (книги или главы) + document_type: str # 'book' или 'chapter' + model: Optional[str] = None + size: str = "1024x1024" + + +class GenerateImageResponse(BaseModel): + image_id: str + filename: str + path: str + prompt: str + model: str + data_url: str # base64 изображение для немедленного отображения + document_id: str # ID документа, к которому привязано изображение + document_type: str # 'book' или 'chapter' diff --git a/backend/models/bible.py b/backend/models/bible.py new file mode 100644 index 0000000..ee8beed --- /dev/null +++ b/backend/models/bible.py @@ -0,0 +1,46 @@ +from typing import List, Literal, Optional +from uuid import uuid4 + +from pydantic import BaseModel, Field + + +class Character(BaseModel): + id: str = Field(default_factory=lambda: str(uuid4())) + name: str + description: str = "" + traits: List[str] = [] # Характер, привычки + appearance: str = "" # Внешность + role: Literal["protagonist", "antagonist", "supporting", "minor"] = "supporting" + + +class Location(BaseModel): + id: str = Field(default_factory=lambda: str(uuid4())) + name: str + description: str = "" + atmosphere: str = "" # Атмосфера, ощущения + type: Literal["city", "building", "nature", "interior", "other"] = "other" + + +class Event(BaseModel): + id: str = Field(default_factory=lambda: str(uuid4())) + title: str + description: str = "" + chapter: Optional[str] = None # В какой главе произошло + importance: Literal["major", "minor", "background"] = "minor" + + +class StoryThread(BaseModel): + id: str = Field(default_factory=lambda: str(uuid4())) + title: str + description: str = "" + status: Literal["active", "resolved", "abandoned"] = "active" + related_characters: List[str] = [] # ID персонажей + related_events: List[str] = [] # ID событий + + +class StoryBible(BaseModel): + characters: List[Character] = [] + locations: List[Location] = [] + events: List[Event] = [] + threads: List[StoryThread] = [] + last_updated: Optional[str] = None # ISO timestamp diff --git a/backend/models/document.py b/backend/models/document.py new file mode 100644 index 0000000..af0f251 --- /dev/null +++ b/backend/models/document.py @@ -0,0 +1,51 @@ +from datetime import datetime +from typing import Optional +from uuid import uuid4 + +from pydantic import BaseModel, Field + + +class DocumentNode(BaseModel): + """Иерархическая модель документа: Книга → Глава → Сцена""" + + id: str = Field(default_factory=lambda: str(uuid4())) + project_id: str + parent_id: Optional[str] = None + title: str = Field(..., min_length=1, max_length=200) + content: str = "" + type: str = Field(default="scene", pattern="^(book|chapter|scene)$") + order: int = 0 + created_at: str = Field(default_factory=lambda: datetime.now().isoformat()) + updated_at: str = Field(default_factory=lambda: datetime.now().isoformat()) + + +# --- Модели для обратной совместимости (старый плоский формат) --- + + +class DocumentBase(BaseModel): + """Базовая модель документа (legacy)""" + + title: str = Field(..., min_length=1, max_length=200) + content: str = "" + type: str = Field(default="scene", pattern="^(scene|chapter|bible)$") + + +class DocumentCreate(DocumentBase): + """Модель для создания нового документа (legacy)""" + + pass + + +class DocumentUpdate(BaseModel): + """Модель для обновления контента документа (legacy)""" + + content: str + + +class DocumentResponse(DocumentBase): + """Модель ответа с документом (legacy)""" + + id: str + + class Config: + from_attributes = True diff --git a/backend/models/project.py b/backend/models/project.py new file mode 100644 index 0000000..cc2de58 --- /dev/null +++ b/backend/models/project.py @@ -0,0 +1,15 @@ +from datetime import datetime +from uuid import uuid4 + +from pydantic import BaseModel, Field + + +class Project(BaseModel): + id: str = Field(default_factory=lambda: str(uuid4())) + user_id: str = ( + "" # Привязка к пользователю (пустой для старых проектов до миграции) + ) + title: str + description: str = "" + created_at: str = Field(default_factory=lambda: datetime.now().isoformat()) + updated_at: str = Field(default_factory=lambda: datetime.now().isoformat()) diff --git a/backend/models/user.py b/backend/models/user.py new file mode 100644 index 0000000..2f7c164 --- /dev/null +++ b/backend/models/user.py @@ -0,0 +1,28 @@ +from uuid import uuid4 + +from pydantic import BaseModel + + +class User(BaseModel): + id: str + username: str + email: str + hashed_password: str + + +class UserCreate(BaseModel): + username: str + email: str + password: str + + +class UserLogin(BaseModel): + username: str + password: str + + +class Token(BaseModel): + access_token: str + token_type: str + user_id: str + username: str diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f0b627f --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,8 @@ +fastapi +uvicorn[standard] +pydantic +httpx +python-dotenv +bcrypt +python-jose[cryptography] +weasyprint diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/routers/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/routers/ai.py b/backend/routers/ai.py new file mode 100644 index 0000000..f9c8264 --- /dev/null +++ b/backend/routers/ai.py @@ -0,0 +1,428 @@ +from config.settings import settings +from fastapi import APIRouter, Depends, HTTPException +from models.ai import ( + AutoUpdateBibleRequest, + AutoUpdateBibleResponse, + CleanupDuplicatesResponse, + ExtractedEntities, + ExtractEntitiesRequest, + ExtractEntitiesResponse, + GenerateImageRequest, + GenerateImageResponse, + GenerateRequest, + GenerateResponse, + PreviewContextRequest, + PreviewContextResponse, +) +from models.bible import Character, Event, Location, StoryBible +from models.user import User +from services.bible_service import bible_service +from services.context_builder import context_builder +from services.deduplication import deduplication_service +from services.entity_extractor import entity_extractor +from services.image_generator import image_generator +from services.llm_router import llm_router +from services.model_config import model_config + +from routers.auth import get_current_user + +router = APIRouter() + + +def _get_user_project(project_id: str, current_user: User) -> None: + """Проверить что проект принадлежит пользователю""" + from services.project_service import project_service + + project = project_service.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + if project.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + + +@router.get("/ai/providers/status") +async def get_providers_status(): + """Проверить, какие провайдеры настроены и готовы к использованию""" + return { + "providers": settings.validate(), + "openrouter_configured": bool( + settings.OPENROUTER_API_KEY + and settings.OPENROUTER_API_KEY != "your_api_key_here" + ), + "openrouter_url": settings.OPENROUTER_BASE_URL, + "ollama_url": settings.OLLAMA_BASE_URL, + } + + +@router.get("/ai/models") +async def get_available_models(provider: str = "ollama"): + """Получить список доступных моделей для указанного провайдера""" + try: + models = model_config.get_available_models(provider) + default_model = model_config.get_default_model(provider) + return { + "provider": provider, + "default_model": default_model, + "models": models, + } + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post("/ai/generate", response_model=GenerateResponse) +async def generate_text(request: GenerateRequest): + try: + # Обогащаем промпт контекстом из Story Bible + enriched_prompt = context_builder.enrich_prompt( + original_prompt=request.prompt, + selected_text=request.selected_text or "", + bible=request.story_bible, + ) + + # Генерируем текст с обогащенным промптом + generated_text = await llm_router.generate( + prompt=enriched_prompt, + provider=request.provider, + model=request.model, + api_key=request.api_key, + preset=request.preset, + options=request.options, + ) + return GenerateResponse( + generated_text=generated_text, + provider=request.provider, + model=request.model or "default", + preset=request.preset, + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/ai/preview-context", response_model=PreviewContextResponse) +async def preview_context(request: PreviewContextRequest): + """Предпросмотр контекста, который будет добавлен к промпту""" + mentioned = context_builder.extract_mentioned_entities( + text=request.text, + bible=request.story_bible, + ) + + context_str = context_builder.build_context_string(mentioned) + + return PreviewContextResponse( + context_string=context_str, + mentioned_characters=[c.name for c in mentioned["characters"]], + mentioned_locations=[l.name for l in mentioned["locations"]], + mentioned_events=[e.title for e in mentioned["events"]], + ) + + +@router.post("/ai/extract-entities", response_model=ExtractEntitiesResponse) +async def extract_entities(request: ExtractEntitiesRequest): + """Извлечение сущностей из текста с помощью AI""" + try: + # Используем OpenRouter с моделью, оптимальной для извлечения + provider = "openrouter" + model = "qwen/qwen-2.5-7b-instruct" + + if ( + not settings.OPENROUTER_API_KEY + or settings.OPENROUTER_API_KEY == "your_api_key_here" + ): + raise ValueError( + "OpenRouter API key not configured. " + "Entity extraction requires a cloud model. " + "Please set OPENROUTER_API_KEY in backend/.env" + ) + + entities = await entity_extractor.extract_entities( + text=request.text, + existing_bible=request.existing_bible, + provider=provider, + model=model, + ) + + return ExtractEntitiesResponse( + entities=ExtractedEntities(**entities), + model_used=model, + provider=provider, + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/ai/auto-update-bible", response_model=AutoUpdateBibleResponse) +async def auto_update_bible( + request: AutoUpdateBibleRequest, current_user: User = Depends(get_current_user) +): + """Автоматическое обновление Story Bible при сохранении сцены""" + if not request.auto_update_enabled: + return AutoUpdateBibleResponse(updated=False, message="Auto-update disabled") + + _get_user_project(request.project_id, current_user) + + try: + # Получаем текущую Story Bible проекта + current_bible = bible_service.get_bible(request.project_id, current_user.id) + + # Извлекаем сущности из текста сцены + entities = await entity_extractor.extract_entities( + text=request.scene_text, + existing_bible=current_bible, + provider="openrouter", + model="qwen/qwen-2.5-7b-instruct", + ) + + # Добавляем новые сущности в Bible + new_chars_count = 0 + new_locs_count = 0 + new_events_count = 0 + + # Добавляем новых персонажей С ДЕДУПЛИКАЦИЕЙ + for char_data in entities.get("new_characters", []): + try: + char_name = char_data.get("name", "") + if not char_name: + continue + + # Ищем существующего персонажа с похожим именем + existing_char = deduplication_service.find_existing_character( + char_name, current_bible + ) + + if existing_char: + # Объединяем данные с существующим персонажем + deduplication_service.merge_character_data(existing_char, char_data) + print(f"Merged character: {char_name} -> {existing_char.name}") + else: + # Создаем нового персонажа + character = Character(**char_data) + current_bible.characters.append(character) + new_chars_count += 1 + print(f"Added new character: {char_name}") + except Exception as e: + print(f"Failed to process character: {e}") + + # Добавляем новые локации С ДЕДУПЛИКАЦИЕЙ + for loc_data in entities.get("new_locations", []): + try: + loc_name = loc_data.get("name", "") + if not loc_name: + continue + + existing_loc = deduplication_service.find_existing_location( + loc_name, current_bible + ) + + if existing_loc: + # Обновляем описание, если новое длиннее + new_desc = loc_data.get("description", "") + if new_desc and len(new_desc) > len(existing_loc.description): + existing_loc.description = new_desc + + new_atm = loc_data.get("atmosphere", "") + if new_atm and not existing_loc.atmosphere: + existing_loc.atmosphere = new_atm + + print(f"Merged location: {loc_name} -> {existing_loc.name}") + else: + location = Location(**loc_data) + current_bible.locations.append(location) + new_locs_count += 1 + print(f"Added new location: {loc_name}") + except Exception as e: + print(f"Failed to process location: {e}") + + # Добавляем новые события С ДЕДУПЛИКАЦИЕЙ + for event_data in entities.get("new_events", []): + try: + event_title = event_data.get("title", "") + if not event_title: + continue + + existing_event = deduplication_service.find_existing_event( + event_title, current_bible + ) + + if existing_event: + # Обновляем описание, если новое длиннее + new_desc = event_data.get("description", "") + if new_desc and len(new_desc) > len(existing_event.description): + existing_event.description = new_desc + print(f"Merged event: {event_title} -> {existing_event.title}") + else: + event = Event(**event_data) + current_bible.events.append(event) + new_events_count += 1 + print(f"Added new event: {event_title}") + except Exception as e: + print(f"Failed to process event: {e}") + + # Обновляем черты существующих персонажей + for trait_update in entities.get("updated_traits", []): + char_name = trait_update.get("character_name") + new_traits = trait_update.get("new_traits", []) + + for char in current_bible.characters: + if char.name.lower() == (char_name or "").lower(): + # Добавляем новые черты, которых еще нет + for trait in new_traits: + if trait not in char.traits: + char.traits.append(trait) + + # Сохраняем обновленную Bible + if new_chars_count > 0 or new_locs_count > 0 or new_events_count > 0: + bible_service.update_bible( + request.project_id, current_bible, current_user.id + ) + message = ( + f"Story Bible обновлена: +{new_chars_count} персонажей, " + f"+{new_locs_count} локаций, +{new_events_count} событий" + ) + else: + message = "Новых сущностей не обнаружено" + + return AutoUpdateBibleResponse( + updated=True, + new_characters=new_chars_count, + new_locations=new_locs_count, + new_events=new_events_count, + message=message, + ) + + except Exception as e: + print(f"Auto-update bible error: {e}") + return AutoUpdateBibleResponse( + updated=False, + message=f"Ошибка при обновлении: {str(e)}", + ) + + +@router.post("/ai/extract-entities/test") +async def test_extraction(): + """Тестовый эндпоинт с примером текста""" + test_text = """ + Алекс вошел в таверну "Серебряный дракон". Внутри пахло жареным мясом и элем. + За стойкой стоял хозяин — крепкий мужчина по имени Гаррет с рыжей бородой. + В углу сидела таинственная женщина в красном плаще — Алекс узнал Лиру. + """ + + sample_bible = StoryBible( + characters=[ + Character(name="Алекс", description="Главный герой", role="protagonist"), + Character( + name="Лира", description="Таинственная проводница", role="supporting" + ), + ] + ) + + try: + entities = await entity_extractor.extract_entities( + text=test_text, + existing_bible=sample_bible, + provider="openrouter", + model="qwen/qwen-2.5-7b-instruct", + ) + + return { + "test_text": test_text, + "extracted": entities, + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post( + "/projects/{project_id}/bible/cleanup-duplicates", + response_model=CleanupDuplicatesResponse, +) +async def cleanup_duplicates( + project_id: str, current_user: User = Depends(get_current_user) +): + """Очистка дубликатов в Story Bible""" + _get_user_project(project_id, current_user) + try: + bible = bible_service.get_bible(project_id, current_user.id) + + # Удаляем дубликаты персонажей + unique_chars = [] + removed_chars = 0 + for char in bible.characters: + is_duplicate = False + for existing in unique_chars: + if deduplication_service.is_similar_name(char.name, existing.name): + # Объединяем данные + deduplication_service.merge_character_data( + existing, char.model_dump() + ) + is_duplicate = True + removed_chars += 1 + break + if not is_duplicate: + unique_chars.append(char) + bible.characters = unique_chars + + # Удаляем дубликаты локаций + unique_locs = [] + removed_locs = 0 + for loc in bible.locations: + is_duplicate = False + for existing in unique_locs: + if deduplication_service.is_similar_name(loc.name, existing.name): + is_duplicate = True + removed_locs += 1 + break + if not is_duplicate: + unique_locs.append(loc) + bible.locations = unique_locs + + # Удаляем дубликаты событий + unique_events = [] + removed_events = 0 + for event in bible.events: + is_duplicate = False + for existing in unique_events: + if deduplication_service.is_similar_name(event.title, existing.title): + is_duplicate = True + removed_events += 1 + break + if not is_duplicate: + unique_events.append(event) + bible.events = unique_events + + # Сохраняем очищенную Bible + bible_service.update_bible(project_id, bible, current_user.id) + + return CleanupDuplicatesResponse( + removed_characters=removed_chars, + removed_locations=removed_locs, + removed_events=removed_events, + message=f"Удалено дубликатов: {removed_chars} персонажей, {removed_locs} локаций, {removed_events} событий", + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/ai/generate-image", response_model=GenerateImageResponse) +async def generate_image( + request: GenerateImageRequest, current_user: User = Depends(get_current_user) +): + """Генерация изображения по промпту""" + try: + result = await image_generator.generate_image( + prompt=request.prompt, + project_id=request.project_id, + user_id=current_user.id, + document_id=request.document_id, + document_type=request.document_type, + model=request.model, + size=request.size, + ) + return result + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/routers/auth.py b/backend/routers/auth.py new file mode 100644 index 0000000..769e35b --- /dev/null +++ b/backend/routers/auth.py @@ -0,0 +1,50 @@ +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from jose import JWTError, jwt +from models.user import Token, User, UserCreate, UserLogin +from services.auth_service import ALGORITHM, SECRET_KEY, auth_service + +router = APIRouter() +security = HTTPBearer() + + +@router.post("/auth/register", response_model=Token) +async def register(user_create: UserCreate): + try: + user = auth_service.create_user(user_create) + return auth_service.get_token(user.username, user_create.password) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post("/auth/login", response_model=Token) +async def login(user_login: UserLogin): + try: + return auth_service.get_token(user_login.username, user_login.password) + except ValueError: + raise HTTPException(status_code=401, detail="Invalid username or password") + + +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), +) -> User: + try: + payload = jwt.decode( + credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM] + ) + username: str = payload.get("sub") + user_id: str = payload.get("user_id") + if username is None or user_id is None: + raise HTTPException(status_code=401, detail="Invalid token") + + user = auth_service.get_user_by_username(username) + if not user or user.id != user_id: + raise HTTPException(status_code=401, detail="User not found") + return user + except JWTError: + raise HTTPException(status_code=401, detail="Invalid token") + + +@router.get("/auth/me", response_model=User) +async def get_me(current_user: User = Depends(get_current_user)): + return current_user diff --git a/backend/routers/bible.py b/backend/routers/bible.py new file mode 100644 index 0000000..dd360f0 --- /dev/null +++ b/backend/routers/bible.py @@ -0,0 +1,83 @@ +from fastapi import APIRouter, Depends, HTTPException +from models.bible import Character, Event, Location, StoryBible, StoryThread +from models.user import User +from services.bible_service import bible_service + +from routers.auth import get_current_user + +router = APIRouter() + + +def _get_user_project(project_id: str, current_user: User) -> None: + """Проверить что проект принадлежит пользователю""" + from services.project_service import project_service + + project = project_service.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + if project.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + + +@router.get("/projects/{project_id}/bible", response_model=StoryBible) +async def get_story_bible( + project_id: str, current_user: User = Depends(get_current_user) +): + """Получить Story Bible проекта""" + _get_user_project(project_id, current_user) + return bible_service.get_bible(project_id, current_user.id) + + +@router.put("/projects/{project_id}/bible", response_model=StoryBible) +async def update_story_bible( + project_id: str, + bible: StoryBible, + current_user: User = Depends(get_current_user), +): + """Обновить всю Story Bible проекта""" + _get_user_project(project_id, current_user) + return bible_service.update_bible(project_id, bible, current_user.id) + + +@router.post("/projects/{project_id}/bible/characters", response_model=Character) +async def add_character( + project_id: str, + character: Character, + current_user: User = Depends(get_current_user), +): + """Добавить нового персонажа в проект""" + _get_user_project(project_id, current_user) + return bible_service.add_character(project_id, character, current_user.id) + + +@router.post("/projects/{project_id}/bible/locations", response_model=Location) +async def add_location( + project_id: str, + location: Location, + current_user: User = Depends(get_current_user), +): + """Добавить новую локацию в проект""" + _get_user_project(project_id, current_user) + return bible_service.add_location(project_id, location, current_user.id) + + +@router.post("/projects/{project_id}/bible/events", response_model=Event) +async def add_event( + project_id: str, + event: Event, + current_user: User = Depends(get_current_user), +): + """Добавить новое событие в проект""" + _get_user_project(project_id, current_user) + return bible_service.add_event(project_id, event, current_user.id) + + +@router.post("/projects/{project_id}/bible/threads", response_model=StoryThread) +async def add_thread( + project_id: str, + thread: StoryThread, + current_user: User = Depends(get_current_user), +): + """Добавить новую сюжетную нить в проект""" + _get_user_project(project_id, current_user) + return bible_service.add_thread(project_id, thread, current_user.id) diff --git a/backend/routers/documents.py b/backend/routers/documents.py new file mode 100644 index 0000000..d8a9641 --- /dev/null +++ b/backend/routers/documents.py @@ -0,0 +1,151 @@ +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, Depends, HTTPException +from models.document import DocumentNode +from models.user import User +from pydantic import BaseModel +from services.document_service import document_service + +from routers.auth import get_current_user + +router = APIRouter() + + +class CreateDocumentRequest(BaseModel): + title: str + type: str # "book", "chapter", "scene" + parent_id: Optional[str] = None + content: str = "" + + +class UpdateDocumentRequest(BaseModel): + title: Optional[str] = None + content: Optional[str] = None + parent_id: Optional[str] = None # Для переноса + order: Optional[int] = None + + +def _get_user_project(project_id: str, current_user: User) -> None: + """Проверить что проект принадлежит пользователю""" + from services.project_service import project_service + + project = project_service.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + if project.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + + +@router.get( + "/projects/{project_id}/documents/tree", response_model=List[Dict[str, Any]] +) +async def get_documents_tree( + project_id: str, current_user: User = Depends(get_current_user) +): + """Получить документы в виде дерева""" + _get_user_project(project_id, current_user) + return document_service.get_documents_tree(project_id, current_user.id) + + +@router.get("/projects/{project_id}/documents", response_model=List[DocumentNode]) +async def get_all_documents( + project_id: str, current_user: User = Depends(get_current_user) +): + """Получить все документы (плоский список)""" + _get_user_project(project_id, current_user) + return document_service.get_all_documents(project_id, current_user.id) + + +@router.get( + "/projects/{project_id}/documents/{document_id}", response_model=DocumentNode +) +async def get_document( + project_id: str, + document_id: str, + current_user: User = Depends(get_current_user), +): + """Получить документ по ID""" + _get_user_project(project_id, current_user) + document = document_service.get_document(project_id, document_id, current_user.id) + if not document: + raise HTTPException(status_code=404, detail="Document not found") + return document + + +@router.post( + "/projects/{project_id}/documents", response_model=DocumentNode, status_code=201 +) +async def create_document( + project_id: str, + request: CreateDocumentRequest, + current_user: User = Depends(get_current_user), +): + """Создать новый документ""" + _get_user_project(project_id, current_user) + try: + return document_service.create_document( + project_id=project_id, + title=request.title, + doc_type=request.type, + user_id=current_user.id, + parent_id=request.parent_id, + content=request.content, + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.put( + "/projects/{project_id}/documents/{document_id}", response_model=DocumentNode +) +async def update_document( + project_id: str, + document_id: str, + request: UpdateDocumentRequest, + current_user: User = Depends(get_current_user), +): + """Обновить документ (включая перенос)""" + _get_user_project(project_id, current_user) + try: + document = document_service.update_document( + project_id=project_id, + document_id=document_id, + user_id=current_user.id, + title=request.title, + content=request.content, + parent_id=request.parent_id, + order=request.order, + ) + if not document: + raise HTTPException(status_code=404, detail="Document not found") + return document + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.delete("/projects/{project_id}/documents/{document_id}") +async def delete_document( + project_id: str, + document_id: str, + current_user: User = Depends(get_current_user), +): + """Удалить документ с каскадом""" + _get_user_project(project_id, current_user) + success = document_service.delete_document(project_id, document_id, current_user.id) + if not success: + raise HTTPException(status_code=404, detail="Document not found") + return {"status": "deleted", "document_id": document_id} + + +@router.get( + "/projects/{project_id}/documents/{parent_id}/children", + response_model=List[DocumentNode], +) +async def get_children( + project_id: str, + parent_id: str, + current_user: User = Depends(get_current_user), +): + """Получить прямых детей узла""" + _get_user_project(project_id, current_user) + return document_service.get_children(project_id, parent_id, current_user.id) diff --git a/backend/routers/export.py b/backend/routers/export.py new file mode 100644 index 0000000..47de709 --- /dev/null +++ b/backend/routers/export.py @@ -0,0 +1,64 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import Response +from models.user import User +from pydantic import BaseModel +from services.export_service import export_service + +from routers.auth import get_current_user + +router = APIRouter() + + +class ExportRequest(BaseModel): + project_id: str + document_id: Optional[str] = ( + None # ID книги или главы. Если None — экспортируем всю книгу + ) + format: str # 'epub', 'fb2', 'pdf', 'docx' + + +def _get_user_project(project_id: str, current_user: User) -> None: + """Проверить что проект принадлежит пользователю""" + from services.project_service import project_service + + project = project_service.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + if project.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + + +@router.post("/export") +async def export_book( + request: ExportRequest, current_user: User = Depends(get_current_user) +): + _get_user_project(request.project_id, current_user) + try: + file_content = export_service.export_document( + project_id=request.project_id, + user_id=current_user.id, + document_id=request.document_id, + format=request.format, + ) + + # Определяем MIME-тип + mime_types = { + "epub": "application/epub+zip", + "fb2": "application/fb2", + "pdf": "application/pdf", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + } + + filename = f"export.{request.format}" + + return Response( + content=file_content, + media_type=mime_types.get(request.format, "application/octet-stream"), + headers={"Content-Disposition": f"attachment; filename={filename}"}, + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/routers/health.py b/backend/routers/health.py new file mode 100644 index 0000000..37dada9 --- /dev/null +++ b/backend/routers/health.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/health") +async def health_check(): + return {"status": "ok", "message": "Writer IDE API is running"} diff --git a/backend/routers/images.py b/backend/routers/images.py new file mode 100644 index 0000000..26a153d --- /dev/null +++ b/backend/routers/images.py @@ -0,0 +1,40 @@ +from pathlib import Path + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import FileResponse +from models.user import User +from services.project_service import project_service + +from routers.auth import get_current_user + +router = APIRouter() + + +@router.get("/images/{project_id}/{filename}") +async def get_image( + project_id: str, filename: str, current_user: User = Depends(get_current_user) +): + """Отдаёт изображение из папки проекта""" + try: + project_dir = project_service.get_project_dir(project_id, current_user.id) + image_path = project_dir / "images" / filename + + if not image_path.exists(): + raise HTTPException(status_code=404, detail="Image not found") + + # Определяем MIME тип по расширению + suffix = Path(filename).suffix.lower() + mime_types = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".gif": "image/gif", + } + + return FileResponse( + path=image_path, + media_type=mime_types.get(suffix, "application/octet-stream"), + ) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) diff --git a/backend/routers/projects.py b/backend/routers/projects.py new file mode 100644 index 0000000..0316b6c --- /dev/null +++ b/backend/routers/projects.py @@ -0,0 +1,85 @@ +from fastapi import APIRouter, Depends, HTTPException +from models.project import Project +from models.user import User +from pydantic import BaseModel +from services.project_service import project_service + +from routers.auth import get_current_user + +router = APIRouter() + + +class CreateProjectRequest(BaseModel): + title: str + description: str = "" + + +class UpdateProjectRequest(BaseModel): + title: str | None = None + description: str | None = None + + +@router.get("/projects", response_model=list[Project]) +async def get_all_projects(current_user: User = Depends(get_current_user)): + """Получить все проекты текущего пользователя""" + return project_service.get_all_projects(user_id=current_user.id) + + +@router.get("/projects/{project_id}", response_model=Project) +async def get_project(project_id: str, current_user: User = Depends(get_current_user)): + """Получить проект по ID""" + project = project_service.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + if project.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + return project + + +@router.post("/projects", response_model=Project, status_code=201) +async def create_project( + request: CreateProjectRequest, current_user: User = Depends(get_current_user) +): + """Создать новый проект""" + return project_service.create_project( + title=request.title, + user_id=current_user.id, + description=request.description, + ) + + +@router.put("/projects/{project_id}", response_model=Project) +async def update_project( + project_id: str, + request: UpdateProjectRequest, + current_user: User = Depends(get_current_user), +): + """Обновить проект""" + project = project_service.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + if project.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + + updated = project_service.update_project( + project_id=project_id, + title=request.title, + description=request.description, + ) + if not updated: + raise HTTPException(status_code=404, detail="Project not found") + return updated + + +@router.delete("/projects/{project_id}") +async def delete_project( + project_id: str, current_user: User = Depends(get_current_user) +): + """Удалить проект""" + try: + success = project_service.delete_project(project_id, current_user.id) + if not success: + raise HTTPException(status_code=404, detail="Project not found") + return {"status": "deleted", "project_id": project_id} + except ValueError as e: + raise HTTPException(status_code=403, detail=str(e)) diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/auth_service.py b/backend/services/auth_service.py new file mode 100644 index 0000000..1a38234 --- /dev/null +++ b/backend/services/auth_service.py @@ -0,0 +1,109 @@ +import json +from datetime import datetime, timedelta +from pathlib import Path +from typing import Optional +from uuid import uuid4 + +import bcrypt + +# Настройки JWT (читаются из config/settings) +from config.settings import settings +from jose import JWTError, jwt +from models.user import Token, User, UserCreate + +SECRET_KEY = settings.JWT_SECRET_KEY +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 дней + + +class AuthService: + def __init__(self): + from config.settings import settings + + self.data_dir = Path(settings.DATA_DIR) + self.data_dir.mkdir(exist_ok=True) + self.users_file = self.data_dir / "users.json" + self.users = self._load_users() + + def _load_users(self) -> list[User]: + if self.users_file.exists(): + with open(self.users_file, "r", encoding="utf-8") as f: + data = json.load(f) + return [User(**u) for u in data] + return [] + + def _save_users(self): + with open(self.users_file, "w", encoding="utf-8") as f: + json.dump( + [u.model_dump() for u in self.users], f, ensure_ascii=False, indent=2 + ) + + def verify_password(self, plain_password: str, hashed_password: str) -> bool: + return bcrypt.checkpw( + plain_password.encode("utf-8"), hashed_password.encode("utf-8") + ) + + def get_password_hash(self, password: str) -> str: + return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + + def get_user_by_username(self, username: str) -> Optional[User]: + for user in self.users: + if user.username == username: + return user + return None + + def get_user_by_id(self, user_id: str) -> Optional[User]: + for user in self.users: + if user.id == user_id: + return user + return None + + def create_user(self, user_create: UserCreate) -> User: + if self.get_user_by_username(user_create.username): + raise ValueError("Username already exists") + + new_user = User( + id=str(uuid4()), + username=user_create.username, + email=user_create.email, + hashed_password=self.get_password_hash(user_create.password), + ) + self.users.append(new_user) + self._save_users() + + # Создаем папку для проектов пользователя + user_projects_dir = self.data_dir / "users" / new_user.id / "projects" + user_projects_dir.mkdir(parents=True, exist_ok=True) + + return new_user + + def authenticate_user(self, username: str, password: str) -> Optional[User]: + user = self.get_user_by_username(username) + if not user or not self.verify_password(password, user.hashed_password): + return None + return user + + def create_access_token(self, data: dict) -> str: + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + def get_token(self, username: str, password: str) -> Token: + user = self.authenticate_user(username, password) + if not user: + raise ValueError("Invalid username or password") + + access_token = self.create_access_token( + data={"sub": user.username, "user_id": user.id} + ) + return Token( + access_token=access_token, + token_type="bearer", + user_id=user.id, + username=user.username, + ) + + +# Singleton +auth_service = AuthService() diff --git a/backend/services/bible_service.py b/backend/services/bible_service.py new file mode 100644 index 0000000..0dcd08a --- /dev/null +++ b/backend/services/bible_service.py @@ -0,0 +1,135 @@ +import json +from datetime import datetime +from pathlib import Path + +from config.settings import settings +from models.bible import Character, Event, Location, StoryBible, StoryThread + +from services.project_service import project_service + + +class BibleService: + def __init__(self): + from config.settings import settings + + self.data_dir = Path(settings.DATA_DIR) + self.project_service = project_service + + def _get_bible_file(self, project_id: str, user_id: str) -> Path: + """Получить путь к файлу Story Bible проекта""" + project_dir = self.project_service.get_project_dir(project_id, user_id) + return project_dir / "story_bible.json" + + def _load_bible(self, project_id: str, user_id: str) -> StoryBible: + """Загрузить Story Bible проекта""" + bible_file = self._get_bible_file(project_id, user_id) + if bible_file.exists(): + with open(bible_file, "r", encoding="utf-8") as f: + data = json.load(f) + return StoryBible(**data) + else: + # Инициализация с примером данных + return self._create_sample_bible() + + def _create_sample_bible(self) -> StoryBible: + """Создать пример Story Bible для тестирования""" + return StoryBible( + characters=[ + Character( + name="Алекс", + description="Главный герой, бывший архитектор", + traits=["Циничный", "Справедливый", "Наблюдательный"], + appearance="Высокий, темные волосы, шрам на левой руке", + role="protagonist", + ), + Character( + name="Лира", + description="Таинственная проводница", + traits=["Загадочная", "Решительная"], + appearance="Носит красный плащ, зеленые глаза", + role="supporting", + ), + ], + locations=[ + Location( + name="Нижний Город", + description="Старая часть города с узкими улочками", + atmosphere="Вечный туман, запах озона и мокрого камня", + type="city", + ) + ], + events=[ + Event( + title="Встреча с Лирой", + description="Алекс встречает Лиру в тумане Нижнего Города", + chapter="Глава 1", + importance="major", + ) + ], + threads=[ + StoryThread( + title="Тайна хранилища", + description="Поиск древнего хранилища знаний", + status="active", + related_characters=[], + related_events=[], + ) + ], + last_updated=datetime.now().isoformat(), + ) + + def _save_bible(self, project_id: str, bible: StoryBible, user_id: str): + """Сохранить Story Bible проекта""" + bible_file = self._get_bible_file(project_id, user_id) + bible.last_updated = datetime.now().isoformat() + with open(bible_file, "w", encoding="utf-8") as f: + json.dump(bible.model_dump(), f, ensure_ascii=False, indent=2) + + def get_bible(self, project_id: str, user_id: str) -> StoryBible: + """Получить Story Bible проекта""" + return self._load_bible(project_id, user_id) + + def update_bible( + self, project_id: str, new_bible: StoryBible, user_id: str + ) -> StoryBible: + """Обновить Story Bible проекта""" + self._save_bible(project_id, new_bible, user_id) + return new_bible + + def add_character( + self, project_id: str, character: Character, user_id: str + ) -> Character: + """Добавить персонажа в проект""" + bible = self._load_bible(project_id, user_id) + bible.characters.append(character) + self._save_bible(project_id, bible, user_id) + return character + + def add_location( + self, project_id: str, location: Location, user_id: str + ) -> Location: + """Добавить локацию в проект""" + bible = self._load_bible(project_id, user_id) + bible.locations.append(location) + self._save_bible(project_id, bible, user_id) + return location + + def add_event(self, project_id: str, event: Event, user_id: str) -> Event: + """Добавить событие в проект""" + bible = self._load_bible(project_id, user_id) + bible.events.append(event) + self._save_bible(project_id, bible, user_id) + return event + + def add_thread( + self, project_id: str, thread: StoryThread, user_id: str + ) -> StoryThread: + """Добавить сюжетную нить в проект""" + bible = self._load_bible(project_id, user_id) + bible.threads.append(thread) + self._save_bible(project_id, bible, user_id) + return thread + + +# Singleton instance +bible_service = BibleService() diff --git a/backend/services/context_builder.py b/backend/services/context_builder.py new file mode 100644 index 0000000..e9fa229 --- /dev/null +++ b/backend/services/context_builder.py @@ -0,0 +1,123 @@ +import re +from typing import Any, Dict, List, Optional + +from models.bible import Character, Event, Location, StoryBible + + +class ContextBuilder: + def extract_mentioned_entities( + self, text: str, bible: StoryBible + ) -> Dict[str, List[Any]]: + """Извлекает упоминания сущностей из текста""" + text_lower = text.lower() + + mentioned_chars: List[Character] = [] + mentioned_locs: List[Location] = [] + mentioned_events: List[Event] = [] + + # Ищем упоминания персонажей + for char in bible.characters: + name_patterns = [ + char.name.lower(), + char.name.lower().split()[0] if " " in char.name else char.name.lower(), + ] + + for pattern in name_patterns: + if pattern and len(pattern) > 2: + if re.search(r"\b" + re.escape(pattern) + r"\b", text_lower): + mentioned_chars.append(char) + break + + # Ищем упоминания локаций + for loc in bible.locations: + name_patterns = [ + loc.name.lower(), + loc.name.lower().split()[0] if " " in loc.name else loc.name.lower(), + ] + + for pattern in name_patterns: + if pattern and len(pattern) > 2: + if re.search(r"\b" + re.escape(pattern) + r"\b", text_lower): + mentioned_locs.append(loc) + break + + # Ищем упоминания событий (по ключевым словам в названии) + for event in bible.events: + keywords = re.findall(r"\w{4,}", event.title.lower()) + for keyword in keywords: + if keyword in text_lower: + mentioned_events.append(event) + break + + return { + "characters": mentioned_chars, + "locations": mentioned_locs, + "events": mentioned_events, + } + + def build_context_string(self, mentioned_entities: Dict[str, List[Any]]) -> str: + """Строит строку контекста для промпта""" + context_parts: List[str] = [] + + # Добавляем информацию о персонажах + if mentioned_entities["characters"]: + chars_info: List[str] = [] + for char in mentioned_entities["characters"][:5]: + traits_str = ( + ", ".join(char.traits[:3]) if char.traits else "нет описания" + ) + chars_info.append( + f"- {char.name}: {char.description} (черты: {traits_str})" + ) + + context_parts.append("ПЕРСОНАЖИ В СЦЕНЕ:\n" + "\n".join(chars_info)) + + # Добавляем информацию о локациях + if mentioned_entities["locations"]: + locs_info: List[str] = [] + for loc in mentioned_entities["locations"][:3]: + atm_str = f"Атмосфера: {loc.atmosphere}" if loc.atmosphere else "" + locs_info.append(f"- {loc.name}: {loc.description} {atm_str}".strip()) + + context_parts.append("ЛОКАЦИИ:\n" + "\n".join(locs_info)) + + # Добавляем информацию о событиях + if mentioned_entities["events"]: + events_info: List[str] = [] + for event in mentioned_entities["events"][:3]: + events_info.append(f"- {event.title}: {event.description}") + + context_parts.append("СОБЫТИЯ:\n" + "\n".join(events_info)) + + return "\n\n".join(context_parts) + + def enrich_prompt( + self, + original_prompt: str, + selected_text: str, + bible: Optional[StoryBible], + ) -> str: + """Обогащает промпт контекстом из Story Bible""" + if not bible or not selected_text: + return original_prompt + + mentioned = self.extract_mentioned_entities(selected_text, bible) + + if not any(mentioned.values()): + return original_prompt + + context_str = self.build_context_string(mentioned) + + enriched_prompt = ( + f"КОНТЕКСТ ПРОИЗВЕДЕНИЯ:\n{context_str}\n\n" + f"ВАЖНО: При выполнении задачи учитывай приведенный контекст. " + f"Сохраняй консистентность с описаниями персонажей, локаций и событий. " + f"Не противоречь установленным фактам.\n\n" + f"ЗАДАЧА:\n{original_prompt}" + ) + + return enriched_prompt + + +# Singleton instance +context_builder = ContextBuilder() diff --git a/backend/services/deduplication.py b/backend/services/deduplication.py new file mode 100644 index 0000000..85dfdde --- /dev/null +++ b/backend/services/deduplication.py @@ -0,0 +1,133 @@ +import re +from typing import List, Optional, Tuple + +from models.bible import Character, Event, Location, StoryBible + + +class DeduplicationService: + # Общие слова, которые нужно удалять при нормализации + CHARACTER_PREFIXES = [ + "котенок", + "кот", + "господин", + "мистер", + "доктор", + "профессор", + "мальчик", + "девочка", + "пёс", + "собака", + ] + LOCATION_PREFIXES = ["город", "деревня", "село", "улица", "площадь"] + + def normalize_name(self, name: str, entity_type: str = "character") -> str: + """Нормализует имя сущности для сравнения""" + # Приводим к нижнему регистру + normalized = name.lower().strip() + + # Убираем лишние пробелы + normalized = re.sub(r"\s+", " ", normalized) + + # Убираем префиксы в зависимости от типа + prefixes = ( + self.CHARACTER_PREFIXES + if entity_type == "character" + else self.LOCATION_PREFIXES + ) + for prefix in prefixes: + if normalized.startswith(prefix + " "): + normalized = normalized[len(prefix) :].strip() + + return normalized + + def is_similar_name(self, name1: str, name2: str) -> bool: + """Проверяет, являются ли два имени вариантами одного и того же""" + norm1 = self.normalize_name(name1) + norm2 = self.normalize_name(name2) + + # Точное совпадение после нормализации + if norm1 == norm2: + return True + + # Одно имя содержит другое (например, "Кекс" и "котенок Кекс") + if norm1 in norm2 or norm2 in norm1: + return True + + # Расстояние Левенштейна (простая реализация) + distance = self._levenshtein_distance(norm1, norm2) + max_len = max(len(norm1), len(norm2)) + if max_len > 0: + similarity = 1 - (distance / max_len) + if similarity > 0.7: # 70% сходства + return True + + return False + + def _levenshtein_distance(self, s1: str, s2: str) -> int: + """Вычисляет расстояние Левенштейна между двумя строками""" + if len(s1) < len(s2): + return self._levenshtein_distance(s2, s1) + + if len(s2) == 0: + return len(s1) + + previous_row = range(len(s2) + 1) + for i, c1 in enumerate(s1): + current_row = [i + 1] + for j, c2 in enumerate(s2): + insertions = previous_row[j + 1] + 1 + deletions = current_row[j] + 1 + substitutions = previous_row[j] + (c1 != c2) + current_row.append(min(insertions, deletions, substitutions)) + previous_row = current_row + + return previous_row[-1] + + def find_existing_character( + self, name: str, bible: StoryBible + ) -> Optional[Character]: + """Ищет существующего персонажа с похожим именем""" + for char in bible.characters: + if self.is_similar_name(name, char.name): + return char + return None + + def find_existing_location( + self, name: str, bible: StoryBible + ) -> Optional[Location]: + """Ищет существующую локацию с похожим именем""" + for loc in bible.locations: + if self.is_similar_name(name, loc.name): + return loc + return None + + def find_existing_event(self, title: str, bible: StoryBible) -> Optional[Event]: + """Ищет существующее событие с похожим названием""" + for event in bible.events: + if self.is_similar_name(title, event.title): + return event + return None + + def merge_character_data(self, existing: Character, new_data: dict) -> Character: + """Объединяет данные нового персонажа с существующим""" + # Добавляем новые черты, которых еще нет + new_traits = new_data.get("traits", []) + for trait in new_traits: + if trait and trait not in existing.traits: + existing.traits.append(trait) + + # Обновляем описание, если новое длиннее и содержательнее + new_desc = new_data.get("description", "") + if new_desc and len(new_desc) > len(existing.description): + existing.description = new_desc + + # Обновляем внешность, если она была пустой + new_appearance = new_data.get("appearance", "") + if new_appearance and not existing.appearance: + existing.appearance = new_appearance + + return existing + + +# Singleton instance +deduplication_service = DeduplicationService() diff --git a/backend/services/document_service.py b/backend/services/document_service.py new file mode 100644 index 0000000..6e5bb5c --- /dev/null +++ b/backend/services/document_service.py @@ -0,0 +1,302 @@ +""" +Сервис для работы с документами (иерархическая структура: Книга → Глава → Сцена) +Документы хранятся в папке проекта: data/projects/{project_id}/documents.json +""" + +import json +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional +from uuid import uuid4 + +from models.document import DocumentNode + +from services.project_service import project_service + + +class DocumentService: + def __init__(self): + self.project_service = project_service + + def _get_documents_file(self, project_id: str, user_id: str) -> Path: + project_dir = self.project_service.get_project_dir(project_id, user_id) + return project_dir / "documents.json" + + def _load_documents(self, project_id: str, user_id: str) -> List[DocumentNode]: + documents_file = self._get_documents_file(project_id, user_id) + if documents_file.exists(): + with open(documents_file, "r", encoding="utf-8") as f: + data = json.load(f) + + # Старый формат: dict, где ключи — ID документов + if isinstance(data, dict): + flat_list = list(data.values()) + if flat_list and len(flat_list) > 0: + return self._migrate_flat_to_hierarchical( + flat_list, project_id, user_id + ) + return [] + + # Новый формат: list + if isinstance(data, list) and len(data) > 0: + # Миграция: если старые данные без parent_id + first = data[0] + if isinstance(first, dict) and "parent_id" not in first: + return self._migrate_flat_to_hierarchical( + data, project_id, user_id + ) + return [DocumentNode(**doc) for doc in data] + return [] + + def _migrate_flat_to_hierarchical( + self, old_data: List[Dict], project_id: str, user_id: str + ) -> List[DocumentNode]: + """Миграция плоских документов в иерархию""" + book = DocumentNode( + project_id=project_id, + title="Книга 1", + type="book", + order=0, + ) + + chapter = DocumentNode( + project_id=project_id, + parent_id=book.id, + title="Глава 1", + type="chapter", + order=0, + ) + + scenes = [] + for i, old_doc in enumerate(old_data): + scene = DocumentNode( + id=old_doc.get("id", str(uuid4())), + project_id=project_id, + parent_id=chapter.id, + title=old_doc.get("title", f"Сцена {i + 1}"), + content=old_doc.get("content", ""), + type="scene", + order=i, + ) + scenes.append(scene) + + result = [book, chapter] + scenes + self._save_documents(project_id, result, user_id) + print(f"Migrated {len(old_data)} flat documents to hierarchical structure") + return result + + def _save_documents( + self, project_id: str, documents: List[DocumentNode], user_id: str + ): + documents_file = self._get_documents_file(project_id, user_id) + with open(documents_file, "w", encoding="utf-8") as f: + json.dump( + [doc.model_dump() for doc in documents], + f, + ensure_ascii=False, + indent=2, + ) + + def get_documents_tree(self, project_id: str, user_id: str) -> List[Dict[str, Any]]: + """Получить документы в виде дерева""" + documents = self._load_documents(project_id, user_id) + return self._build_tree(documents) + + def _build_tree(self, documents: List[DocumentNode]) -> List[Dict[str, Any]]: + """Строит дерево из плоского списка""" + doc_map = {doc.id: {**doc.model_dump(), "children": []} for doc in documents} + roots = [] + + for doc in documents: + node = doc_map[doc.id] + if doc.parent_id is None: + roots.append(node) + elif doc.parent_id in doc_map: + doc_map[doc.parent_id]["children"].append(node) + + def sort_tree(nodes): + nodes.sort(key=lambda x: x["order"]) + for node in nodes: + sort_tree(node["children"]) + + sort_tree(roots) + return roots + + def get_all_documents(self, project_id: str, user_id: str) -> List[DocumentNode]: + """Получить все документы (плоский список)""" + return self._load_documents(project_id, user_id) + + def get_document( + self, project_id: str, document_id: str, user_id: str + ) -> Optional[DocumentNode]: + documents = self._load_documents(project_id, user_id) + for doc in documents: + if doc.id == document_id: + return doc + return None + + def create_document( + self, + project_id: str, + title: str, + doc_type: str, + user_id: str, + parent_id: Optional[str] = None, + content: str = "", + ) -> DocumentNode: + """Создать новый документ с валидацией иерархии""" + documents = self._load_documents(project_id, user_id) + + # Валидация иерархии + if doc_type == "chapter" and parent_id: + parent = next((d for d in documents if d.id == parent_id), None) + if not parent or parent.type != "book": + raise ValueError("Глава может быть создана только внутри книги") + + if doc_type == "scene" and parent_id: + parent = next((d for d in documents if d.id == parent_id), None) + if not parent or parent.type != "chapter": + raise ValueError("Сцена может быть создана только внутри главы") + + if doc_type == "book" and parent_id: + raise ValueError("Книга не может иметь родителя") + + # Вычисляем order (последний среди братьев + 1) + siblings = [d for d in documents if d.parent_id == parent_id] + max_order = max([s.order for s in siblings], default=-1) + + document = DocumentNode( + project_id=project_id, + parent_id=parent_id, + title=title, + content=content, + type=doc_type, + order=max_order + 1, + ) + + documents.append(document) + self._save_documents(project_id, documents, user_id) + return document + + def update_document( + self, + project_id: str, + document_id: str, + user_id: str, + title: Optional[str] = None, + content: Optional[str] = None, + parent_id: Optional[str] = None, + order: Optional[int] = None, + ) -> Optional[DocumentNode]: + """Обновить документ (включая перенос)""" + documents = self._load_documents(project_id, user_id) + + for i, doc in enumerate(documents): + if doc.id == document_id: + # Валидация при переносе + if parent_id is not None and parent_id != doc.parent_id: + new_parent = next((d for d in documents if d.id == parent_id), None) + + if doc.type == "book": + raise ValueError("Книга не может быть перемещена") + + if doc.type == "chapter" and ( + not new_parent or new_parent.type != "book" + ): + raise ValueError("Глава может находиться только внутри книги") + + if doc.type == "scene" and ( + not new_parent or new_parent.type != "chapter" + ): + raise ValueError("Сцена может находиться только внутри главы") + + documents[i].parent_id = parent_id + + if title is not None: + documents[i].title = title + if content is not None: + documents[i].content = content + if order is not None: + documents[i].order = order + + documents[i].updated_at = datetime.now().isoformat() + self._save_documents(project_id, documents, user_id) + return documents[i] + + return None + + def delete_document(self, project_id: str, document_id: str, user_id: str) -> bool: + """Удалить документ с каскадом (дети тоже удаляются)""" + documents = self._load_documents(project_id, user_id) + + target = next((d for d in documents if d.id == document_id), None) + if not target: + return False + + ids_to_delete: set[str] = set() + + def collect_descendants(parent_id: str): + ids_to_delete.add(parent_id) + children = [d for d in documents if d.parent_id == parent_id] + for child in children: + collect_descendants(child.id) + + collect_descendants(document_id) + + documents = [d for d in documents if d.id not in ids_to_delete] + self._save_documents(project_id, documents, user_id) + return True + + def get_children( + self, project_id: str, parent_id: str, user_id: str + ) -> List[DocumentNode]: + """Получить прямых детей узла""" + documents = self._load_documents(project_id, user_id) + children = [d for d in documents if d.parent_id == parent_id] + children.sort(key=lambda x: x.order) + return children + + def initialize_test_document( + self, project_id: str, user_id: str + ) -> List[DocumentNode]: + """ + Создаёт тестовую иерархию документов при первом запуске, + если файл документов пуст. + """ + documents = self._load_documents(project_id, user_id) + + if documents: + return documents + + book = DocumentNode( + project_id=project_id, + title="Книга 1", + type="book", + order=0, + ) + + chapter = DocumentNode( + project_id=project_id, + parent_id=book.id, + title="Глава 1", + type="chapter", + order=0, + ) + + scene = DocumentNode( + project_id=project_id, + parent_id=chapter.id, + title="Сцена 1", + content="Это тестовый документ для разработки Writer IDE. Здесь будет располагаться контент ваших сценариев.", + type="scene", + order=0, + ) + + result = [book, chapter, scene] + self._save_documents(project_id, result, user_id) + print(f"Created test hierarchical documents for project: {project_id}") + return result + + +# Singleton instance +document_service = DocumentService() diff --git a/backend/services/entity_extractor.py b/backend/services/entity_extractor.py new file mode 100644 index 0000000..41aaf2b --- /dev/null +++ b/backend/services/entity_extractor.py @@ -0,0 +1,134 @@ +import json +import re +from typing import Any, Dict, List, Optional + +from models.bible import StoryBible + +from services.llm_router import llm_router + + +class EntityExtractor: + def __init__(self): + self.llm_router = llm_router + + def _build_extraction_prompt( + self, text: str, existing_bible: Optional[StoryBible] + ) -> str: + """Формирует промпт для извлечения сущностей""" + + existing_chars = "" + existing_locs = "" + existing_events = "" + + if existing_bible: + if existing_bible.characters: + existing_chars = "Известные персонажи: " + ", ".join( + [c.name for c in existing_bible.characters] + ) + if existing_bible.locations: + existing_locs = "Известные локации: " + ", ".join( + [l.name for l in existing_bible.locations] + ) + if existing_bible.events: + existing_events = "Известные события: " + ", ".join( + [e.title for e in existing_bible.events] + ) + + prompt = f"""Ты — эксперт по анализу художественных текстов. Твоя задача — извлечь из текста новых персонажей, локации и события. + +ВАЖНО: Отвечай СТРОГО НА РУССКОМ ЯЗЫКЕ. Все имена, описания и названия должны быть на русском языке. + +{existing_chars} +{existing_locs} +{existing_events} + +ТЕКСТ ДЛЯ АНАЛИЗА: +--- +{text} +--- + +ИНСТРУКЦИИ: +1. Извлеки ТОЛЬКО НОВЫЕ сущности, которых еще нет в списке известных +2. Для персонажей укажи: name, description (краткое), traits (список черт), appearance (если описана), role (protagonist/antagonist/supporting/minor) +3. Для локаций укажи: name, description, atmosphere (если есть), type (city/building/nature/interior/other) +4. Для событий укажи: title, description, importance (major/minor/background) +5. Если обнаружены новые черты для УЖЕ ИЗВЕСТНЫХ персонажей, укажи их в updated_traits +6. НЕ выдумывай информацию, которой нет в тексте +7. Если новых сущностей нет, верни пустые списки +8. Все ответы СТРОГО НА РУССКОМ ЯЗЫКЕ + +ОТВЕТЬ СТРОГО В ФОРМАТЕ JSON (без markdown, без пояснений, только чистый JSON): +{{ + "new_characters": [ + {{"name": "...", "description": "...", "traits": ["...", "..."], "appearance": "...", "role": "..."}} + ], + "new_locations": [ + {{"name": "...", "description": "...", "atmosphere": "...", "type": "..."}} + ], + "new_events": [ + {{"title": "...", "description": "...", "importance": "..."}} + ], + "updated_traits": [ + {{"character_name": "...", "new_traits": ["...", "..."]}} + ] +}}""" + + return prompt + + def _parse_json_response(self, response_text: str) -> Dict[str, Any]: + """Парсит JSON из ответа модели, обрабатывая возможные markdown обертки""" + # Убираем возможные markdown обертки + cleaned = response_text.strip() + + # Убираем ```json и ``` + if cleaned.startswith("```"): + cleaned = re.sub(r"^```(?:json)?\s*", "", cleaned) + cleaned = re.sub(r"\s*```$", "", cleaned) + + # Пытаемся найти JSON в тексте + json_match = re.search(r"\{[\s\S]*\}", cleaned) + if json_match: + cleaned = json_match.group(0) + + try: + return json.loads(cleaned) + except json.JSONDecodeError as e: + raise ValueError( + f"Failed to parse JSON from LLM response: {e}\n" + f"Raw response: {response_text[:500]}" + ) + + async def extract_entities( + self, + text: str, + existing_bible: Optional[StoryBible] = None, + provider: str = "openrouter", + model: Optional[str] = None, + ) -> Dict[str, Any]: + """Извлекает сущности из текста""" + + prompt = self._build_extraction_prompt(text, existing_bible) + + # Используем пресет editor для точности (низкая температура) + generated_text = await self.llm_router.generate( + prompt=prompt, + provider=provider, + model=model, + preset="editor", + ) + + parsed = self._parse_json_response(generated_text) + + # Валидация и нормализация ответа + result = { + "new_characters": parsed.get("new_characters", []), + "new_locations": parsed.get("new_locations", []), + "new_events": parsed.get("new_events", []), + "updated_traits": parsed.get("updated_traits", []), + } + + return result + + +# Singleton instance +entity_extractor = EntityExtractor() diff --git a/backend/services/export_service.py b/backend/services/export_service.py new file mode 100644 index 0000000..9d5d213 --- /dev/null +++ b/backend/services/export_service.py @@ -0,0 +1,239 @@ +""" +Сервис экспорта документов в различные форматы (EPUB, FB2, PDF, DOCX) через Pandoc. +""" + +import os +import shutil +import subprocess +import tempfile +from typing import List + +from fastapi import HTTPException +from models.document import DocumentNode + +from services.document_service import document_service +from services.project_service import project_service + + +class ExportService: + def __init__(self): + # Проверка наличия pandoc при инициализации + try: + subprocess.run(["pandoc", "--version"], capture_output=True, check=True) + self.available = True + except FileNotFoundError: + print("WARNING: Pandoc not found. Export functionality will be limited.") + self.available = False + + def _get_document_and_children( + self, documents: List[DocumentNode], target_id: str + ) -> List[DocumentNode]: + """Рекурсивно получает документ и всех его потомков, отсортированных по order.""" + result: List[DocumentNode] = [] + doc_map = {d.id: d for d in documents} + + def collect_children(parent_id: str): + children = [d for d in documents if d.parent_id == parent_id] + children.sort(key=lambda x: x.order) + for child in children: + result.append(child) + collect_children(child.id) + + if target_id in doc_map: + result.append(doc_map[target_id]) + collect_children(target_id) + + return result + + def _build_book_html( + self, + project_id: str, + user_id: str, + document_id: str | None = None, + base_path: str = "", # Абсолютный путь для относительных ресурсов + ) -> str: + """Собирает HTML-контент книги или конкретного документа с изображениями.""" + documents = document_service.get_all_documents(project_id, user_id) + project_dir = project_service.get_project_dir(project_id, user_id) + images_dir = project_dir / "images" + + # Проверяем наличие обложки + cover_path = images_dir / "cover.png" + has_cover = cover_path.exists() + + if document_id: + target_docs = self._get_document_and_children(documents, document_id) + else: + # Экспортируем всё: берем корневые документы (книги) и всех потомков + tree = document_service.get_documents_tree(project_id, user_id) + target_docs = self._flatten_tree(tree) + + # Собираем все PNG-файлы изображений + all_images = [] + if images_dir.exists(): + all_images = [ + f.name + for f in images_dir.iterdir() + if f.is_file() and f.suffix.lower() == ".png" + ] + + # Определяем ID всех глав для поиска привязанных изображений + chapter_ids = {d.id for d in documents if d.type == "chapter"} + + # "Бесхозные" изображения — не cover.png и не {chapter_id}.png + # (старые файлы с UUID, сгенерированные до введения конвенции) + orphan_images = [ + img + for img in all_images + if img != "cover.png" and img.removesuffix(".png") not in chapter_ids + ] + orphan_index = ( + 0 # Индекс для последовательного использования orphan-изображений + ) + + # Формируем префикс пути: абсолютный путь если задан, иначе относительный + img_prefix = f"{base_path}/images" if base_path else "images" + + html_parts: List[str] = [] + + # Добавляем обложку, если есть и это полная книга (не один документ) + if has_cover and not document_id: + html_parts.append( + '
' + ) + html_parts.append( + f'Cover' + ) + html_parts.append("
") + html_parts.append("

") + + for doc in target_docs: + if doc.type == "book": + html_parts.append(f"

{doc.title}

") + elif doc.type == "chapter": + # Ищем изображение главы: сначала по конвенции {chapter_id}.png + chapter_image_path = images_dir / f"{doc.id}.png" + has_chapter_image = chapter_image_path.exists() + chapter_image_filename = f"{doc.id}.png" if has_chapter_image else None + + # Fallback: если нет файла по конвенции, берём "бесхозное" изображение + if not has_chapter_image and orphan_index < len(orphan_images): + chapter_image_filename = orphan_images[orphan_index] + orphan_index += 1 + + html_parts.append(f"

{doc.title}

") + + # Вставляем изображение главы, если есть + if chapter_image_filename: + html_parts.append( + '
' + ) + html_parts.append( + f'{doc.title}' + ) + html_parts.append("
") + elif doc.type == "scene" and doc.content: + html_parts.append(f"

{doc.title}

") + html_parts.append(doc.content) + html_parts.append("

") + + return "\n".join(html_parts) + + def _flatten_tree(self, tree: List[dict]) -> List[DocumentNode]: + """Разворачивает дерево документов в плоский список в порядке обхода.""" + result: List[DocumentNode] = [] + for node in tree: + result.append(DocumentNode(**node)) + for child in node.get("children", []): + result.extend(self._flatten_tree([child])) + return result + + def export_document( + self, project_id: str, user_id: str, document_id: str | None, format: str + ) -> bytes: + """Экспортирует документ (или всю книгу) в указанный формат.""" + if not self.available: + raise HTTPException( + status_code=503, + detail="Pandoc is not installed. Export functionality is unavailable.", + ) + + valid_formats = ["epub", "fb2", "pdf", "docx"] + if format not in valid_formats: + raise HTTPException( + status_code=400, + detail=f"Unsupported format. Choose from {valid_formats}", + ) + + # Получаем проект для метаданных + project = project_service.get_project(project_id) + title = project.title if project else "Без названия" + + # Собираем HTML + html_content = self._build_book_html(project_id, user_id, document_id) + + # Получаем путь к папке проекта для копирования изображений + project_dir = project_service.get_project_dir(project_id, user_id) + images_dir = project_dir / "images" + + # Создаём временную директорию + temp_dir = tempfile.mkdtemp() + temp_html = os.path.join(temp_dir, "content.html") + output_path = os.path.join(temp_dir, f"output.{format}") + + # Копируем папку images во временную директорию для Pandoc + if images_dir.exists(): + shutil.copytree(images_dir, os.path.join(temp_dir, "images")) + + try: + # Записываем HTML + html_content = self._build_book_html( + project_id, user_id, document_id, base_path=temp_dir + ) + with open(temp_html, "w", encoding="utf-8") as f: + f.write(html_content) + + # Формируем команду pandoc + cmd = [ + "pandoc", + temp_html, + "-o", + output_path, + "--from", + "html", + "--to", + format, + ] + + # Метаданные для EPUB/FB2 + if format in ["epub", "fb2"]: + cmd.extend(["--metadata", f"title={title}"]) + cmd.extend(["--metadata", "author=Author"]) + # Указываем путь к ресурсам (изображениям) + cmd.extend(["--resource-path", temp_dir]) + + # PDF через WeasyPrint (не требует LaTeX) + if format == "pdf": + cmd.extend(["--pdf-engine=weasyprint"]) + + # Выполняем + subprocess.run(cmd, check=True, capture_output=True, text=True) + + # Читаем результат + with open(output_path, "rb") as f: + file_content = f.read() + + return file_content + + except subprocess.CalledProcessError as e: + raise HTTPException(status_code=500, detail=f"Pandoc error: {e.stderr}") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + # Очистка временных файлов + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + + +# Singleton instance +export_service = ExportService() diff --git a/backend/services/image_generator.py b/backend/services/image_generator.py new file mode 100644 index 0000000..e567b34 --- /dev/null +++ b/backend/services/image_generator.py @@ -0,0 +1,203 @@ +import base64 +import logging +import re +from pathlib import Path +from typing import Optional + +import httpx +from config.settings import settings +from fastapi import HTTPException + +from services.project_service import project_service + +logger = logging.getLogger(__name__) + + +class ImageGenerator: + def __init__(self): + self.api_key = settings.OPENROUTER_API_KEY + self.base_url = getattr( + settings, "OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1" + ) + self.default_model = "qwen/qwen-image-2.1-turbo" + + def _get_images_dir(self, project_id: str, user_id: str) -> Path: + """Получить или создать директорию для изображений проекта""" + project_dir = project_service.get_project_dir(project_id, user_id) + images_dir = project_dir / "images" + images_dir.mkdir(exist_ok=True) + return images_dir + + async def generate_image( + self, + prompt: str, + project_id: str, + user_id: str, + document_id: str, + document_type: str, # 'book' или 'chapter' + model: Optional[str] = None, + size: str = "1024x1024", + ) -> dict: + """ + Генерирует изображение по промпту и сохраняет его в папку проекта. + Имя файла определяется по конвенции: + - document_type='book' -> cover.png (обложка книги) + - document_type='chapter' -> {document_id}.png (иллюстрация главы) + Возвращает информацию о сохранённом файле. + """ + if not self.api_key or self.api_key == "your_api_key_here": + raise ValueError("OpenRouter API key not configured") + + model = model or self.default_model + + # Генерация изображений использует OpenAI Images API endpoint, + # а не Chat Completions. См. официальную документацию модели. + payload = { + "model": model, + "prompt": prompt, + "n": 1, + "response_format": "b64_json", + } + + # Мапим size в aspect_ratio для моделей, которые его поддерживают + aspect_ratio_map = { + "1024x1024": "1:1", + "1024x1792": "9:16", + "1792x1024": "16:9", + "512x512": "1:1", + } + if size in aspect_ratio_map: + payload["size"] = size + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "HTTP-Referer": "http://localhost:3000", + "X-Title": "Writer IDE", + } + + try: + async with httpx.AsyncClient(timeout=120.0) as client: + response = await client.post( + f"{self.base_url}/images/generations", + json=payload, + headers=headers, + ) + response.raise_for_status() + data = response.json() + + # Проверяем наличие ошибки в ответе (некоторые провайдеры + # возвращают 200 с полем error вместо HTTP error status) + if "error" in data: + error_info = data["error"] + error_msg = ( + error_info.get("message", str(error_info)) + if isinstance(error_info, dict) + else str(error_info) + ) + raise ValueError(f"API returned error: {error_msg}") + + # OpenAI Images API формат: {"data": [{"b64_json": "..."}]} + images_data = data.get("data", []) + if not images_data: + raise ValueError( + f"No image data in response. Keys: {list(data.keys())}" + ) + + img_data = images_data[0] + image_url = None + image_base64 = None + + if isinstance(img_data, dict): + image_base64 = img_data.get("b64_json") + image_url = img_data.get("url") + elif isinstance(img_data, str): + # Некоторые провайдеры могут вернуть URL напрямую в data + image_url = img_data if img_data.startswith("http") else None + + # Фоллбэк: пробуем извлечь из text content (если провайдер + # использует chat/completions формат поверх images/generations) + if not image_url and not image_base64: + content = ( + data.get("choices", [{}])[0].get("message", {}).get("content", "") + ) + if content: + base64_match = re.search( + r"data:image/[^;]+;base64,([A-Za-z0-9+/=]+)", content + ) + if base64_match: + image_base64 = base64_match.group(1) + + if not image_url and not image_base64: + logger.warning( + "Could not extract image. Response keys: %s", + list(data.keys()), + ) + raise ValueError( + f"Could not extract image from response. Available keys: {list(data.keys())}" + ) + + # Сохраняем изображение + images_dir = self._get_images_dir(project_id, user_id) + + # Определяем имя файла по конвенции + if document_type == "book": + filename = "cover.png" # Обложка книги + else: # chapter + filename = f"{document_id}.png" # Изображение главы + + filepath = images_dir / filename + + if image_url: + async with httpx.AsyncClient(timeout=60.0) as client: + img_response = await client.get(image_url) + img_response.raise_for_status() + image_bytes = img_response.content + filepath.write_bytes(image_bytes) + data_url = f"data:image/png;base64,{base64.b64encode(image_bytes).decode('ascii')}" + elif image_base64: + image_bytes = base64.b64decode(image_base64) + filepath.write_bytes(image_bytes) + # Определяем MIME тип по первым байтам изображения + mime = "image/png" # default + if image_bytes[:2] == b"\xff\xd8": + mime = "image/jpeg" + elif image_bytes[:8] == b"\x89PNG\r\n\x1a\n": + mime = "image/png" + elif image_bytes[:6] in (b"GIF87a", b"GIF89a"): + mime = "image/gif" + elif image_bytes[:4] == b"RIFF" and image_bytes[8:12] == b"WEBP": + mime = "image/webp" + data_url = f"data:{mime};base64,{image_base64}" + else: + # Должно быть обработано выше, но на всякий случай + raise ValueError("No image data available to encode") + + return { + "image_id": document_id, + "filename": filename, + "path": f"/api/images/{project_id}/{filename}", + "prompt": prompt, + "model": model, + "data_url": data_url, + "document_id": document_id, + "document_type": document_type, + } + + except httpx.HTTPStatusError as e: + raise HTTPException( + status_code=502, + detail=f"Image generation API error: {e.response.status_code} - {e.response.text[:200]}", + ) + except httpx.TimeoutException: + raise HTTPException(status_code=504, detail="Image generation timed out") + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Image generation failed: {str(e)}" + ) + + +# Singleton +image_generator = ImageGenerator() diff --git a/backend/services/llm_router.py b/backend/services/llm_router.py new file mode 100644 index 0000000..61c06a9 --- /dev/null +++ b/backend/services/llm_router.py @@ -0,0 +1,126 @@ +from typing import Any, Dict, Optional + +import httpx +from config.settings import settings + +from services.model_config import model_config + + +class LLMRouter: + def __init__(self): + self.ollama_base_url = settings.OLLAMA_BASE_URL + self.openrouter_base_url = settings.OPENROUTER_BASE_URL + + async def generate_ollama( + self, + prompt: str, + model: Optional[str] = None, + preset: str = "creative_draft", + options: Optional[Dict[str, Any]] = None, + ) -> str: + """Генерация через локальную Ollama с пресетами""" + from services.presets import get_preset + + # Используем модель из конфига, если не указана + model = model or model_config.get_default_model("ollama") + + # Валидация модели + if not model_config.validate_model("ollama", model): + raise ValueError(f"Model {model} not available for Ollama") + + # Получаем базовые параметры из пресета + preset_options = get_preset(preset, "ollama") + # Объединяем с пользовательскими опциями (пользовательские переопределяют пресет) + merged_options = {**preset_options, **(options or {})} + + async with httpx.AsyncClient() as client: + payload = { + "model": model, + "prompt": prompt, + "stream": False, + "options": merged_options, + } + response = await client.post( + f"{self.ollama_base_url}/api/generate", json=payload, timeout=60.0 + ) + response.raise_for_status() + return response.json()["response"] + + async def generate_openrouter( + self, + prompt: str, + model: Optional[str] = None, + api_key: Optional[str] = None, + preset: str = "creative_draft", + options: Optional[Dict[str, Any]] = None, + ) -> str: + """Генерация через OpenRouter API с пресетами""" + from services.presets import get_preset + + # Используем ключ из settings, если не передан явно + actual_api_key = api_key or settings.OPENROUTER_API_KEY + + # Валидация API ключа + if not actual_api_key or actual_api_key == "your_api_key_here": + raise ValueError( + "OpenRouter API key not configured. " + "Please set OPENROUTER_API_KEY in backend/.env file" + ) + + # Используем модель из конфига, если не указана + model = model or model_config.get_default_model("openrouter") + + # Валидация модели + if not model_config.validate_model("openrouter", model): + raise ValueError(f"Model {model} not available for OpenRouter") + + # Получаем базовые параметры из пресета + preset_options = get_preset(preset, "openrouter") + # Объединяем с пользовательскими опциями + merged_options = {**preset_options, **(options or {})} + + async with httpx.AsyncClient() as client: + payload = { + "model": model, + "messages": [{"role": "user", "content": prompt}], + } + # Добавляем объединённые опции (включая frequency_penalty, presence_penalty и temperature) + if merged_options: + payload.update(merged_options) + headers = { + "Authorization": f"Bearer {actual_api_key}", + "Content-Type": "application/json", + "HTTP-Referer": "http://localhost:3000", # Для OpenRouter + "X-Title": "Writer IDE", # Для OpenRouter + } + response = await client.post( + f"{self.openrouter_base_url}/chat/completions", + json=payload, + headers=headers, + timeout=60.0, + ) + response.raise_for_status() + return response.json()["choices"][0]["message"]["content"] + + async def generate( + self, + prompt: str, + provider: str = "ollama", + model: Optional[str] = None, + api_key: Optional[str] = None, + preset: str = "creative_draft", + options: Optional[Dict[str, Any]] = None, + ) -> str: + """Универсальный метод генерации""" + if provider == "ollama": + return await self.generate_ollama(prompt, model, preset, options) + elif provider == "openrouter": + return await self.generate_openrouter( + prompt, model, api_key, preset, options + ) + else: + raise ValueError(f"Unknown provider: {provider}") + + +# Singleton instance +llm_router = LLMRouter() diff --git a/backend/services/model_config.py b/backend/services/model_config.py new file mode 100644 index 0000000..9d39561 --- /dev/null +++ b/backend/services/model_config.py @@ -0,0 +1,47 @@ +import json +from pathlib import Path +from typing import Any, Dict, List, Optional + + +class ModelConfig: + """Класс для работы с конфигурацией моделей AI провайдеров""" + + def __init__(self, config_path: str = "config/models.json"): + self.config_path = Path(config_path) + self.config = self._load_config() + + def _load_config(self) -> Dict[str, Any]: + """Загрузить конфигурацию из JSON файла""" + if not self.config_path.exists(): + raise FileNotFoundError(f"Config file not found: {self.config_path}") + + with open(self.config_path, "r", encoding="utf-8") as f: + return json.load(f) + + def get_available_models(self, provider: str) -> List[Dict[str, Any]]: + """Получить список доступных моделей для провайдера""" + if provider not in self.config: + raise ValueError(f"Unknown provider: {provider}") + return self.config[provider]["available_models"] + + def get_default_model(self, provider: str) -> str: + """Получить модель по умолчанию для провайдера""" + if provider not in self.config: + raise ValueError(f"Unknown provider: {provider}") + return self.config[provider]["default_model"] + + def get_model_info(self, provider: str, model_id: str) -> Optional[Dict[str, Any]]: + """Получить информацию о конкретной модели""" + models = self.get_available_models(provider) + for model in models: + if model["id"] == model_id: + return model + return None + + def validate_model(self, provider: str, model_id: str) -> bool: + """Проверить, существует ли модель для провайдера""" + return self.get_model_info(provider, model_id) is not None + + +# Singleton instance +model_config = ModelConfig() diff --git a/backend/services/presets.py b/backend/services/presets.py new file mode 100644 index 0000000..be5249d --- /dev/null +++ b/backend/services/presets.py @@ -0,0 +1,85 @@ +from typing import Any, Literal + +PresetName = Literal["creative_draft", "editor", "brainstorm"] + +# Пресеты для Ollama (параметры, которые понимает Ollama API) +OLLAMA_PRESETS: dict[str, dict[str, Any]] = { + "creative_draft": { + "temperature": 0.85, + "top_p": 0.92, + "top_k": 50, + "repeat_penalty": 1.15, + }, + "editor": { + "temperature": 0.3, + "top_p": 0.85, + "top_k": 40, + "repeat_penalty": 1.0, + }, + "brainstorm": { + "temperature": 1.0, + "top_p": 0.95, + "top_k": 80, + "repeat_penalty": 1.2, + }, +} + +# Пресеты для OpenRouter (параметры OpenAI-совместимого API) +OPENROUTER_PRESETS: dict[str, dict[str, Any]] = { + "creative_draft": { + "temperature": 0.85, + "top_p": 0.92, + "frequency_penalty": 0.3, + "presence_penalty": 0.4, + }, + "editor": { + "temperature": 0.3, + "top_p": 0.85, + "frequency_penalty": 0.0, + "presence_penalty": 0.0, + }, + "brainstorm": { + "temperature": 1.0, + "top_p": 0.95, + "frequency_penalty": 0.5, + "presence_penalty": 0.6, + }, +} + + +def get_preset(preset_name: str, provider: str) -> dict[str, Any]: + """Получить параметры пресета для указанного провайдера. + + Args: + preset_name: Название пресета (creative_draft, editor, brainstorm) + provider: Провайдер ("ollama" или "openrouter") + + Returns: + Параметры пресета как словарь + + Raises: + ValueError: Если провайдер неизвестен или пресет не найден + """ + if provider == "ollama": + return OLLAMA_PRESETS.get(preset_name, OLLAMA_PRESETS["creative_draft"]) + elif provider == "openrouter": + return OPENROUTER_PRESETS.get(preset_name, OPENROUTER_PRESETS["creative_draft"]) + else: + raise ValueError(f"Unknown provider: {provider}") + + +def list_presets(provider: str | None = None) -> dict[str, dict[str, dict[str, Any]]]: + """Вернуть все доступные пресеты. + + Args: + provider: Опционально фильтровать по провайдеру + + Returns: + Словарь всех доступных пресетов + """ + if provider == "ollama": + return OLLAMA_PRESETS + elif provider == "openrouter": + return OPENROUTER_PRESETS + else: + return {"ollama": OLLAMA_PRESETS, "openrouter": OPENROUTER_PRESETS} diff --git a/backend/services/project_service.py b/backend/services/project_service.py new file mode 100644 index 0000000..c514587 --- /dev/null +++ b/backend/services/project_service.py @@ -0,0 +1,196 @@ +import json +import shutil +from datetime import datetime +from pathlib import Path + +from config.settings import settings +from models.bible import StoryBible +from models.project import Project +from models.user import User + +from services.auth_service import auth_service + + +class ProjectService: + def __init__(self): + self.data_dir = Path(settings.DATA_DIR) + self.projects_dir = self.data_dir / "projects" + self.projects_dir.mkdir(parents=True, exist_ok=True) + self.projects_file = self.data_dir / "projects.json" + self.projects = self._load_projects() + + # Миграция: перенос старых проектов в структуру с пользователями + self._migrate_old_data() + + def _load_projects(self) -> list[Project]: + """Загрузить список проектов""" + if self.projects_file.exists(): + with open(self.projects_file, "r", encoding="utf-8") as f: + data = json.load(f) + return [Project(**p) for p in data] + return [] + + def _save_projects(self): + """Сохранить список проектов""" + with open(self.projects_file, "w", encoding="utf-8") as f: + json.dump( + [p.model_dump() for p in self.projects], + f, + ensure_ascii=False, + indent=2, + ) + + def _migrate_old_data(self): + """Миграция старых проектов в структуру data/users/{user_id}/projects/""" + old_projects_dir = self.data_dir / "projects" + users_dir = self.data_dir / "users" + users_file = self.data_dir / "users.json" + default_user_id = "default-user-0000-0000-0000-000000000000" + + # Если есть старые проекты, но нет пользователей — создаём дефолтного и перемещаем + if old_projects_dir.exists() and not users_dir.exists(): + print("Migrating old projects to a default user...") + default_user = User( + id=default_user_id, + username="admin", + email="admin@local", + hashed_password=auth_service.get_password_hash("admin"), + ) + auth_service.users.append(default_user) + auth_service._save_users() + + # Перемещаем папку проектов + new_user_dir = users_dir / default_user.id + new_user_dir.mkdir(parents=True, exist_ok=True) + old_projects_dir.rename(new_user_dir / "projects") + + # Обновляем user_id у существующих проектов + for i, project in enumerate(self.projects): + self.projects[i].user_id = default_user.id + self._save_projects() + + print("Migration complete. Default user: admin / admin") + + # Если проекты уже перемещены, но users.json отсутствует — восстанавливаем + if users_dir.exists() and not users_file.exists(): + has_projects_without_user = any( + p.user_id == "" or p.user_id == default_user_id for p in self.projects + ) + if has_projects_without_user or any(users_dir.iterdir()): + print("Restoring default user (users.json was missing)...") + default_user = User( + id=default_user_id, + username="admin", + email="admin@local", + hashed_password=auth_service.get_password_hash("admin"), + ) + auth_service.users.append(default_user) + auth_service._save_users() + + # Обновляем user_id у проектов без привязки + for i, project in enumerate(self.projects): + if not project.user_id: + self.projects[i].user_id = default_user.id + self._save_projects() + print("Default user restored: admin / admin") + + # Если папка projects уже переехала, обновляем ссылку + if users_dir.exists(): + new_projects_dir = self.data_dir / "users" + # Проверяем что old_projects_dir больше не существует (уже перемещён) + if not old_projects_dir.exists(): + self.projects_dir = new_projects_dir + + def get_project_dir(self, project_id: str, user_id: str) -> Path: + """Получить директорию проекта с учётом пользователя""" + # Определяем базу: если есть users/{user_id}, используем новую структуру + user_projects_base = self.data_dir / "users" / user_id / "projects" + if user_projects_base.exists(): + project_dir = user_projects_base / project_id + else: + # Fallback для обратной совместимости + project_dir = self.data_dir / "projects" / project_id + + if not project_dir.exists(): + raise ValueError(f"Project directory not found: {project_id}") + return project_dir + + def get_all_projects(self, user_id: str | None = None) -> list[Project]: + """Получить все проекты (опционально фильтруя по пользователю)""" + if user_id: + return [p for p in self.projects if p.user_id == user_id] + return self.projects + + def get_project(self, project_id: str) -> Project | None: + """Получить проект по ID""" + for project in self.projects: + if project.id == project_id: + return project + return None + + def create_project( + self, title: str, user_id: str, description: str = "" + ) -> Project: + """Создать новый проект""" + project = Project(title=title, user_id=user_id, description=description) + self.projects.append(project) + self._save_projects() + + # Создаем директорию проекта в папке пользователя + project_dir = self.data_dir / "users" / user_id / "projects" / project.id + project_dir.mkdir(parents=True, exist_ok=True) + + # Инициализируем пустые файлы + (project_dir / "documents.json").write_text("[]", encoding="utf-8") + + # Создаем пустую Story Bible + empty_bible = StoryBible() + (project_dir / "story_bible.json").write_text( + json.dumps(empty_bible.model_dump(), ensure_ascii=False, indent=2), + encoding="utf-8", + ) + + return project + + def update_project( + self, + project_id: str, + title: str | None = None, + description: str | None = None, + ) -> Project | None: + """Обновить проект""" + for i, project in enumerate(self.projects): + if project.id == project_id: + if title is not None: + self.projects[i].title = title + if description is not None: + self.projects[i].description = description + self.projects[i].updated_at = datetime.now().isoformat() + self._save_projects() + return self.projects[i] + return None + + def delete_project(self, project_id: str, user_id: str) -> bool: + """Удалить проект и все его данные""" + for i, project in enumerate(self.projects): + if project.id == project_id: + # Проверяем принадлежность + if project.user_id != user_id: + raise ValueError("Project does not belong to this user") + + self.projects.pop(i) + self._save_projects() + + # Удаляем директорию проекта + project_dir = ( + self.data_dir / "users" / user_id / "projects" / project_id + ) + if project_dir.exists(): + shutil.rmtree(project_dir) + + return True + return False + + +# Singleton instance +project_service = ProjectService() diff --git a/backend/test_docker.sh b/backend/test_docker.sh new file mode 100755 index 0000000..0d76701 --- /dev/null +++ b/backend/test_docker.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Скрипт для сборки и тестирования Docker-образа backend локально + +echo "🔨 Building backend Docker image..." +docker build -t writer-ide-backend:local . + +if [ $? -ne 0 ]; then + echo "❌ Build failed!" + exit 1 +fi + +echo "✅ Build successful!" +echo "" +echo "🔍 Checking installed tools..." +docker run --rm writer-ide-backend:local pandoc --version +docker run --rm writer-ide-backend:local python -c "import weasyprint; print('WeasyPrint version:', weasyprint.__version__)" + +echo "" +echo "🚀 To run the container:" +echo "docker run -p 8000:8000 -v /path/to/data:/app/data --env-file .env.docker writer-ide-backend:local" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ad13f82 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +version: '3.8' + +services: + # Backend (FastAPI) + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: writer-ide-backend + restart: unless-stopped + ports: + - "8000:8000" + env_file: + - .env + environment: + - DATA_DIR=/app/data + - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434} + - ENVIRONMENT=production + volumes: + # Монтируем HDD с данными в контейнер + - ${DATA_VOLUME_PATH:-/mnt/data/writer-ide}:/app/data + # Монтируем .env файл (опционально, если нужен hot-reload) + - ./backend/.env:/app/.env:ro + extra_hosts: + # Для доступа к Ollama на хосте (Linux) + - "host.docker.internal:host-gateway" + networks: + - writer-ide-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Frontend (Next.js) + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: writer-ide-frontend + restart: unless-stopped + ports: + - "3000:3000" + environment: + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8000/api} + - NODE_ENV=production + depends_on: + backend: + condition: service_healthy + networks: + - writer-ide-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + +networks: + writer-ide-network: + driver: bridge + +# Volumes не определяем явно, так как используем bind mount с HDD diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..8c8f14d --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,38 @@ +# Node modules (устанавливаются в Docker) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Next.js build output (собирается в Docker) +.next/ +out/ + +# Git +.git +.gitignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Docker +Dockerfile +docker-compose*.yml +.dockerignore + +# Документация +*.md +!README.md + +# Тесты +tests/ +.pytest_cache/ +.coverage + +# Прочее +.env* +!.env.example +*.log diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 0000000..af3b7c8 --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1,9 @@ + + +## This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..9ece358 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,55 @@ +# Этап 1: Установка зависимостей +FROM node:20-alpine AS deps +WORKDIR /app + +# Копируем package.json и package-lock.json +COPY package.json package-lock.json* ./ + +# Устанавливаем зависимости +RUN npm ci --only=production + +# Этап 2: Сборка приложения +FROM node:20-alpine AS builder +WORKDIR /app + +# Копируем зависимости из предыдущего этапа +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Переменные окружения для сборки (если нужны) +ENV NEXT_TELEMETRY_DISABLED=1 + +# Сборка Next.js +RUN npm run build + +# Этап 3: Production runtime +FROM node:20-alpine AS runner +WORKDIR /app + +# Переменные окружения +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME="0.0.0.0" + +# Создаём группу и пользователя для безопасности +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 nextjs + +# Копируем собранные файлы из builder +# Standalone output содержит только необходимое для запуска +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public + +# Устанавливаем владельца файлов +RUN chown -R nextjs:nodejs /app + +# Переключаемся на непривилегированного пользователя +USER nextjs + +# Открываем порт +EXPOSE 3000 + +# Команда запуска +CMD ["node", "server.js"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000..7c66b09 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', // Критично для Docker! + reactStrictMode: true, +} + +module.exports = nextConfig diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..a0cf0c7 --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,17 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ + cacheComponents: true, + partialPrefetching: true, + turbopack: { + rules: { + "*.css": { + loaders: ["@tailwindcss/turbopack"], + as: "*.css", + }, + }, + }, +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..a517baa --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,7619 @@ +{ + "name": "frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.1.0", + "dependencies": { + "@tailwindcss/typography": "^0.5.20", + "@tiptap/extension-blockquote": "^3.32.2", + "@tiptap/extension-bubble-menu": "^3.32.2", + "@tiptap/extension-color": "^3.32.2", + "@tiptap/extension-font-family": "^3.32.2", + "@tiptap/extension-text-style": "^3.32.2", + "@tiptap/extension-underline": "^3.32.2", + "@tiptap/react": "^3.32.2", + "@tiptap/starter-kit": "^3.32.2", + "axios": "^1.21.0", + "lucide-react": "^1.67.0", + "next": "16.4.4", + "react": "19.3.0", + "react-dom": "19.3.0", + "zustand": "^5.0.16" + }, + "devDependencies": { + "@tailwindcss/turbopack": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.4.4", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.12.tgz", + "integrity": "sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.12", + "@babel/types": "^7.29.11", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", + "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", + "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.12", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.12", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.11", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.4.tgz", + "integrity": "sha512-6/3ZuELR0tPLgNigvv8d/LAmfLIo1IpNfvrI/AHCAl9w9D0DNVGQGvo+OLPzAWR+WMW3vVyO7HaW0kfL97huBg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.4.tgz", + "integrity": "sha512-O3ZZhL+PMlqa8xgg8Sx/UeG1BGgl/jOGkyRZFC8pDZTNYBoIbbUZtwDotNR+aGsnROE321lOQ+MfwFjdVFz+FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.4.tgz", + "integrity": "sha512-1KlZ5IJ7zcLBZvqG1q7nuRWm2LAAN4C3iBqqoBCRRl1UEv0A4k6Rek+x3j23UiWf8Gz+VsN+M/LylAWkxpm2mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.4.tgz", + "integrity": "sha512-Tb7efFdhbS/ph74RS7wob84gXGM58qxoO/WrkiMsTmy/JZktKOKBeiM45W0E/b8JfBzZJdexQqSmvedBwDK0PA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.4.tgz", + "integrity": "sha512-wojSfLnH8RRImhtbcn3Dlx9JICh6aQHgDA33XDr+EPua2tt7Q/yjDn0FTAbqfNlLrBmOASAGhLh6NvBk9L3gzw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.4.tgz", + "integrity": "sha512-zgxpGOZaR+2O6dOk5ZAUkKgH92ZtqNg3ohzyg2HmWc2N+AZH4dEpTwdpB0e5MR9YRVdVDUkEM5g6c3TWWn8g1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.4.tgz", + "integrity": "sha512-fg3Wr030Y3BDkCXi6HGsPz0BUPTGZJS8TZSrCO0tS3O/KBsCyx4eP6aeaizCpGmcUKhtb4l7JoMc88QO2qBl2g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.4.tgz", + "integrity": "sha512-40cr71PSdCV9zU2g0B1XG27af10tYU+9g8F+FtF3qSguQg0jSZDoxn0NrAvgFQ0cy9s0ZBA/00P9g/XL1O43xg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.4.tgz", + "integrity": "sha512-vFMiEbRZ+LYNeswslf4LuGuoPHE8JbtqV0GnmzbpnGNMs0uWaR2q98hu8M/UWGycNJERBXIUss+BGd3PIJ8VZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.4.tgz", + "integrity": "sha512-jy2P4V8BDZ+CFCzYABHYRZ1CmyFhfmYe22i8KA4uEafdYPrUjJ3Ot+lx265mLWLHiZRIfBzCn3V1F17m0vzl9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.4.tgz", + "integrity": "sha512-b7QK/QG04v3j04NbpG82XLCedRyulnkuwUDwRa9oK/ypnJIzMapz2Wg+0TltVczjUzDaJJHIa1+jj9aMUOFMTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.5", + "jiti": "^2.7.0", + "lightningcss": "1.33.0", + "magic-string": "^1.1.0", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.4" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.4.tgz", + "integrity": "sha512-riqsRw2sLF7B1sNCCkTl3KYXEqvhTiABzZ/4oxxHfZ3c3Td60bCBfOrUuUGOiHagd3E7V+zG46c+XBDWZGFBcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.4", + "@tailwindcss/oxide-darwin-arm64": "4.3.4", + "@tailwindcss/oxide-darwin-x64": "4.3.4", + "@tailwindcss/oxide-freebsd-x64": "4.3.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.4", + "@tailwindcss/oxide-linux-x64-musl": "4.3.4", + "@tailwindcss/oxide-wasm32-wasi": "4.3.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.4" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.4.tgz", + "integrity": "sha512-qkjxt+EFocOi3mtsUS3ndlj6yj4SHubwBdvBynLnS8T4mvSzX0Enn0CpypJKmBEtczUAXOHVZKA2zTGzSNU2Sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.4.tgz", + "integrity": "sha512-i/awUA/7mJQAbA3AzCW8aQtUh5goMieUdF1zOoclLaJUCxDvsXQ8VJqpQZY3dfcHS6PJjd3tw25nNneD/mEviQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.4.tgz", + "integrity": "sha512-q56V9U6AnoYz/yj/alkDLhSQ9AZoXQSEl93f7WETa0+32CudbqM57BXdYXfMYPKudueXKaA+MG6L/0KRseF7Jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.4.tgz", + "integrity": "sha512-r0W7A+Z9HTHh7bwihJl45E/zZKLjxD50/jToVF/E/RZHSl8+EWO+4yKKJpPMcUooT9hdoip3Zx1uWnbMg4N79g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.4.tgz", + "integrity": "sha512-3aih/sRxzeYfi2Bkh+bGYNf2YU9Llamb2mpA7BNF1obYzdgEeWDAMQzK6BSwzLhs/eTdOaSur92qrGfchagUtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.4.tgz", + "integrity": "sha512-ktJSrPGQou4OqL88rDEx5CkeM9BjzrwZvYo8u7B7VL1zkBa2XBm351o4MQrek95qikb2M++26Y7w7vbH6Mgl8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.4.tgz", + "integrity": "sha512-rt3hjUxT7utAtcB+df6wHyaXDyxhE7Zd1x7L/nvB5Jb2qBb8dZwFUoWqeRja1BaF5OlSqkKSjBmVXj9OsevmCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.4.tgz", + "integrity": "sha512-XE3jO2yOOWjttpeGvpwf3/b274NMi3QapQSAI/EzsgrmYNan1Tw8dXT9JQkN7K0OgvuqiC8mcCyNvGzEkpVJPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.4.tgz", + "integrity": "sha512-PaAZ2FjJaZk9SQLYtKIfQWiFm/xtl7pX2MKPgCzn0mS/VU54yu+vP457AJdB2XFLxU3gO2abLCt8N7Lg4EbKNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.4.tgz", + "integrity": "sha512-bjO9WmoM687p8AoCIyw1DUqzpAs4JICLvNmIPxIWDmhw8IOsdubtWQLShyJmISnDKtRy1BBApHVOUl6sNFQQJA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.3", + "@emnapi/runtime": "^1.11.3", + "@emnapi/wasi-threads": "^1.2.3", + "@napi-rs/wasm-runtime": "^1.2.2", + "@tybys/wasm-util": "^0.10.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.4.tgz", + "integrity": "sha512-jCrwCvWVtt7ruMXp6dFPd5KBNU8o1tWtEdZbTL/w/hvD3lUYZA0E2mmfvTzW4QLHIx5wGQU9HeY7997R1QTrEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.4.tgz", + "integrity": "sha512-cge1wcedn1FrRSXL22lF5sNDYr8c9+VImk7zFYLPs12jkDmchSrZNfzWqOKub+5Hb/aYRdDEqERGGDKONBlOzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/turbopack": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/turbopack/-/turbopack-4.3.4.tgz", + "integrity": "sha512-zaGyyqTe+7Leowbp5HZ6SXBQ/XFbGK4sV/I5ZPwiO/bWD1j/ukJGzyKOZLj9//uD11790obelpdQHjXJRTpORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.4", + "@tailwindcss/oxide": "4.3.4", + "tailwindcss": "4.3.4" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz", + "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" + } + }, + "node_modules/@tiptap/core": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.32.2.tgz", + "integrity": "sha512-EitM8KY3agYfnp/bqhfxcaReVq4CUqVzMqoSIl7xSfYABTGYoTDtVXUdYSGutcCBFjc86BbXjrOlDlqkgC8BsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.32.2" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.32.2.tgz", + "integrity": "sha512-VLcdnbOm7G52NpUhgbyo3XIOz6Eky7/DyGYuYnCpEUe0TE5xk7Qs6ukrh9vaf2XhyeLDHsmVZuzybGrdtqoC0w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2", + "@tiptap/pm": "3.32.2" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.32.2.tgz", + "integrity": "sha512-5Q8bScZEZbIBB2tBgnc7Cr6XuH2Nt/6l1HP3FO0wWO7nqXPOcodEbjslePI3v1iag+LNWX16BZ8fDrJr/PbxXA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.32.2.tgz", + "integrity": "sha512-mwxWtLHiwhtzXpvdjoegH10eIA8CJr5INGAzUSfzRp4v13zH3rDbsoafljwEi+V5JfcSY/bbXf1akOtsny4yXw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2", + "@tiptap/pm": "3.32.2" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.32.2.tgz", + "integrity": "sha512-YP7IlC3swrNPlnFHIgVDsDl70geqqWtcAkbuncQEs5FJ9UQg432VwVfwCUW4NsG532p5AqlTTwOKcSLS7p/pqA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.32.2" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.32.2.tgz", + "integrity": "sha512-jm7qXuDmBzQYPemm/9chLepQjCNVtZhaoU9N2f4GxdUi8u/fi3AQLP7xlXjRCQUyam50FVqIIWgD567mLQ3sMg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.32.2.tgz", + "integrity": "sha512-KFkPMMgXEgXImGpbUsrHNaERtvOGbJ/JiWn0zHNGSJCf6frn44OLeRFPpEMptmwMqtJxftBVFbEbcPOlATYqDw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2", + "@tiptap/pm": "3.32.2" + } + }, + "node_modules/@tiptap/extension-color": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.32.2.tgz", + "integrity": "sha512-VPu6oygqQyF4+THM+aVg6i8uxVCYxrM/vK/lEzaVVedwVSxZ1G/RliCufmZ/bnGHqgx+c4TDZUqlprKa9nvukQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-text-style": "3.32.2" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.32.2.tgz", + "integrity": "sha512-zKWj8jh1kpckfSwle+kqFn0ZnYvkhb7nz89nrNvpnnsQMbbGU2/+bUAfYU3XSEKZr3VBvW176FtlMPD8ggC3Cg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.32.2.tgz", + "integrity": "sha512-mE9muf4KSY3s+LCaHG/G957pVHVF2q46i+YkZF3ZG70errQcyZ5YFKnce9PSL1fiL0VLL+2UhMAL2QDM5oDiNA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.32.2" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.32.2.tgz", + "integrity": "sha512-JPyJQvJlGDxn2v7Mr8Yft2BmcXaHiapVsXBMYiOJh5BeWbnwUYFWNH9h5PU/xTOz2zVEV79Kh4HmpgXZS3FCbg==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.32.2", + "@tiptap/pm": "3.32.2" + } + }, + "node_modules/@tiptap/extension-font-family": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-3.32.2.tgz", + "integrity": "sha512-C1dys5j/u5aQke8DVzAJl6RFvOFm45KM3bGeL0R62gjMCZRTF9nziLZpZcZm7UVkoUhtMzFEtvA7/QSndUO4aw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-text-style": "3.32.2" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.32.2.tgz", + "integrity": "sha512-7W/iX8h6KQ2HK2SfvzCIgySOEJwTM4owqz1tXVOPiLPvKSvDDa9GQsp60+xNyqDNHuplE8bRsZ5dH1nilgAaAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.32.2" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.32.2.tgz", + "integrity": "sha512-oIdXmya2XBzp3Cw9Nr36agPvr9ZXP3p2fQXaFflxYYBKOG3UUzw38CId7J6K1jYxBf8c1zodbiKFEL7rttJ6fQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.32.2.tgz", + "integrity": "sha512-AE2WNA3jVnPV/cVyTwTmtmGZqX8LiiGicDHXX4bdvfzv+Aez4sgzdkuFjOGWtnEy43NnXdoI1UX7y/984NM0iA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.32.2.tgz", + "integrity": "sha512-Pl0YRS60R339yHrAWGhPT9WHIlD72BO6SGVdj8lBs2OAaYw5RQL/1BGZi+7UvAyc2yJxaRC4XN9X4jdQHDUq9A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2", + "@tiptap/pm": "3.32.2" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.32.2.tgz", + "integrity": "sha512-aqk8OK4Xo59qZni7YSy42/ltQFb2IOmli3YERIq1XPQ4vl5sxoYhL+sFoOPmvbjHvaHZpK6n8vPh1SmAnB570A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.32.2.tgz", + "integrity": "sha512-EwQtMXsU1Vc03n+yud7lfaR+RjHTPwFyI/hcEylvJba7iIXDSLA/ey4mnpwt2od8CEx/ZcOutIryORKVR9Py/Q==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2", + "@tiptap/pm": "3.32.2" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.32.2.tgz", + "integrity": "sha512-rLYk3GXSswrCakyZwo8VW0+yjnYhLJSBRvzClneIrlPhqQ2r/oDUjeqkpm8lGvwREw1lDSh7wefGhkhTRcQkAw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2", + "@tiptap/pm": "3.32.2" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.32.2.tgz", + "integrity": "sha512-vTdPfzZODLVXog/NMs6zcaTOhtlh6ptJidlMg8l1TLhO+YQyEyO/K45aV429D9xTo/BOVazreav/C+hgemSnYg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.32.2" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.32.2.tgz", + "integrity": "sha512-SioQO8TqRb8aq9L+GpI/a/F7HCSBarm3Uvqtt2YttyazjSjEedu1zSnaa93V+WEFmlqyXnxVa24vv2zj6qlavQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.32.2" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.32.2.tgz", + "integrity": "sha512-WQw2fLtJAxCYftKAk+YNlPDc+OxtXK4nNfXRBwr6oC67SoJtoLe0bo5jS6vF+cEqzHjDaboJi+O3MSNp2L2VQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.32.2" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.32.2.tgz", + "integrity": "sha512-3bqC//4GEhUiux3dhW/TJuveBp9ADv4nHlqBdVQa3A+USGbaBvE9YvXxA32wZL0leSRykxEnHbNHZ1J4KjIynQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.32.2.tgz", + "integrity": "sha512-i3d4vCrZySS9g5nGa5W0LS6LPMKxcAnJO6uADTnGVlKK0ewklcWWZbZ9GB/Fpx/IjKAvXWeayjfsNpTae/yVZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.32.2.tgz", + "integrity": "sha512-W6G7UbQ71TyvZvORDdj6u8By8B/Dymy8KoaKGaNHqB82SpM42ZK2TmeFBHI0Lo4wTkZJL3LuI+QthaDvh0USsA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.32.2.tgz", + "integrity": "sha512-7VHyvO8V7xCOj4BY56gqCylZXF972dtk+ngIIXT19nhBXYuQZKXGrN11xPrfMip/1nGsMWov9PZB2HleYJnXGQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.32.2.tgz", + "integrity": "sha512-kY7B5ocKx3JzqIl2kLXGvnLGZRxuYEFzMkpgJ6Vh6/VTTK+JCeOVItYEQGFrAxrodIMmhIrXpi1jF57s6iZswg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.32.2.tgz", + "integrity": "sha512-TUE1JedPjYQ/T527f2a4xz+4Xi1a5nIgW8KwC0DjmqbaHirlz0eCNI2nMEN70pu5WPpqgXmki3lCF3LScQxIGw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2", + "@tiptap/pm": "3.32.2" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.32.2.tgz", + "integrity": "sha512-4cuUbYf55NnYBIvDBK/K1VFVQ4pocKwAZC5TAy4s9dptvazxVHkQVMbiQsyhwfIbM4rxmHWkM/6omaFfTB++/g==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.4.4", + "prosemirror-commands": "^1.7.2", + "prosemirror-dropcursor": "^1.8.4", + "prosemirror-gapcursor": "^1.4.1", + "prosemirror-history": "^1.5.1", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.12", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.5", + "prosemirror-transform": "^1.12.2", + "prosemirror-view": "^1.42.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.32.2.tgz", + "integrity": "sha512-IrraXFTp6S9R+GlCzavMUQel0wK9wafXU3VnaujeC/REi6sYWNuyBe0jICimdbEk1dU32UvlkvPCuIDRPKlzaQ==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "3.32.2", + "@tiptap/extension-floating-menu": "3.32.2" + }, + "peerDependencies": { + "@tiptap/core": "3.32.2", + "@tiptap/pm": "3.32.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.32.2.tgz", + "integrity": "sha512-6K+oOMF4orhnOxivgUTKiSpNAEftHyWOcfFIvIEMUP044b34obBUogzMTfM3RjtewZtQXVafO9zmGU30IllC1g==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "3.32.2", + "@tiptap/extension-blockquote": "3.32.2", + "@tiptap/extension-bold": "3.32.2", + "@tiptap/extension-bullet-list": "3.32.2", + "@tiptap/extension-code": "3.32.2", + "@tiptap/extension-code-block": "3.32.2", + "@tiptap/extension-document": "3.32.2", + "@tiptap/extension-dropcursor": "3.32.2", + "@tiptap/extension-gapcursor": "3.32.2", + "@tiptap/extension-hard-break": "3.32.2", + "@tiptap/extension-heading": "3.32.2", + "@tiptap/extension-horizontal-rule": "3.32.2", + "@tiptap/extension-italic": "3.32.2", + "@tiptap/extension-link": "3.32.2", + "@tiptap/extension-list": "3.32.2", + "@tiptap/extension-list-item": "3.32.2", + "@tiptap/extension-list-keymap": "3.32.2", + "@tiptap/extension-ordered-list": "3.32.2", + "@tiptap/extension-paragraph": "3.32.2", + "@tiptap/extension-strike": "3.32.2", + "@tiptap/extension-text": "3.32.2", + "@tiptap/extension-underline": "3.32.2", + "@tiptap/extensions": "3.32.2", + "@tiptap/pm": "3.32.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@types/estree": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", + "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/type-utils": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", + "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", + "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.13.0.tgz", + "integrity": "sha512-60E5mvmIiW+i1E/HYeSX4iE4A8s9NJSGYPu7UM0DJght1c2tTkWwuyDyAwzsEXhKM/XbbndfnTiXdfrf27d2hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.13.0.tgz", + "integrity": "sha512-qlh/JR3Tuy1cJ7xtf5pR4ykvLiK6os7X7EL2+zXZbrVc/s+yB8DoFdQrCAW9/a2TOxPVbR7QVNpkPWh7UWIsEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.13.0.tgz", + "integrity": "sha512-yynnnnJXUC99WeD1GfvuXPKsKNfvgzGYC2eRayFnUfsHVROjqrbFOkFQLZiSx1H8+PLy5BzU8vqyPyLqbRfeBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.13.0.tgz", + "integrity": "sha512-t8PXE0Ip9tmGyhRlfVsnEbzGM5iSvJIeNIXciUACjzpg0mmPs13fGv2x7W8sBa0S8zbIiohxKXS9fL/LXW/ERQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.13.0.tgz", + "integrity": "sha512-qdtl9GDWDoDx6rhzF4RU5SJutV3lcLOiTiyqD6nBxYfEO6lAgLNukfrA+HJ2u2OiyWvV8JW5iEWD3LoONw7lYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.13.0.tgz", + "integrity": "sha512-VrMUVwdrScTarsh4B6aUhTtziUavSlglo4Hi8XZ6CpAe4+j3nquY+XoP+tYkiSBDsXZ8386ne4pv4YYYBNi02w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.13.0.tgz", + "integrity": "sha512-zSbTVwaxiTC+zYJ9xVNTC6c+BYO/kOV/odcTI3bHuFFXa+XmjLoUkb/8UxsZeU5YF4K000m17wrXoTblNWuzOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.13.0.tgz", + "integrity": "sha512-Z/Rax87moZHsuvJ8awdfP+zrMlsCt3d7mggUPIiPe32KpeagimyzJWnZSMQoV67an9uVQghyh92Pc0VprITnYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.13.0.tgz", + "integrity": "sha512-oUzXz8hNvFLNEAag8lBKJe1euiV2dmGHAR+eQ5fNpsZO0F1K7IMRb4J6pqYsd7uLMRQNPiu5A1WKUJHbFLWbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.13.0.tgz", + "integrity": "sha512-BxhGYvZBg0YrvNOeSEGR3eNklwXYy0mUJk7oeGJvZoQMKM68zYvbbf52+fQI3Le4X0fW276FTKIoTwjpMob80A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.13.0.tgz", + "integrity": "sha512-nZcIuup9Ar3gAlqoZfX/KW6x83TMI11tLpCjldZG5M1KeY8qNUPNJJ71+sPvG8HMBpoh+ix1pAm5gkMFvcSAcQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.13.0.tgz", + "integrity": "sha512-AG6AgdDrkRNjAPwD/cftFJGgZUWCiJX+E8vpYCDEnwFHLTHZ7ppBiU6/vyPPwrfjcPUPQCDbbxE/eZUXsx49jQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.13.0.tgz", + "integrity": "sha512-6vOqYGcAmzpRWpUURWutzvpf1qJ6eOu5cfYObdtPJnzZ0+Kw4cw+ZkZJqIYEh8fPuTBwSYDL/FwlUhma1G/JVQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.13.0.tgz", + "integrity": "sha512-4HeIn0S2KVUowK0A+EweXrDvZ8CKHEfTtif0kx9vt3ORNsqcXNFFIFnHE0tizma3/Gt5OGrU2ovYNuj+D/bN4g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.13.0.tgz", + "integrity": "sha512-JfkvqY7NZbxyhexLE+g6Z8Hsx4utHOGaVazAVghPiBn8Upw2iERZPpKYnW1FBVU4WStv/BiIsoK/Muke0E7asA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.13.0.tgz", + "integrity": "sha512-iwjLeVdZo/8v5OKAeRIC+4VQV045t1EPZr5B/AVLgUQ04IdtNthWzg6not2cNjZgH2gHpSf4fOXMmN83aLO1FA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.13.0.tgz", + "integrity": "sha512-TOh1fqOjyq1OPoht3BDLd7Y+HeMJRwd5g2VNmCQclknEbjp4py5GFRmT4L/rOybF5Af8q0LT9zHs2WGkwlqtrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.13.0.tgz", + "integrity": "sha512-3T2MNtF2dd1yjaXCVqrsi+/3VZeksEzU1hygPzqqkBLVVjn+2iJzXEkxxCKIVFrWnpmEbazeHJp/ruYD0RUVWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.13.0.tgz", + "integrity": "sha512-hKWfSLiN3L4cIxg0jrpyP/66AXLTcyD37jmSn//rGdOUYGlizYxTEFdJ+GZbp2NnaML3YS/A73nCysze8FaJdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.13.0.tgz", + "integrity": "sha512-N4gDKkDoiyIQEcn3vTqe2ksRxUAtBvQ8c5TiT+WonSa6FHgCAY5+RggnZ134hXJoRj57JEjv4/m+i/pAKHWrTQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.13.0.tgz", + "integrity": "sha512-DpLMlAqlrk4zBVK3DiDjwKD0oq7pZNy+8BiIBRuByqb6dm5jsdoLMhIiyh/xR7+WejaW/1Ui+XgbgMNa4+zWJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "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/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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-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.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.2.0.tgz", + "integrity": "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "es-shim-unscopables": "^1.1.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.14.0.tgz", + "integrity": "sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.21.0.tgz", + "integrity": "sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.35.tgz", + "integrity": "sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001820", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz", + "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.473", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.473.tgz", + "integrity": "sha512-9njcRQK+6FOXNAWw5g+h2RPiyazqo5RFMVJ39ZMHjDHUNnnFlhnOGV7O0WmJetODDx19ES5p+FLi+KCVFQGugw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.26.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.1.tgz", + "integrity": "sha512-PPnnSJq3azKOIewXR/uppBV/6h98dprBJ/WaQ9Qgj10xX1lJLNfg+XShpbsrSqcDSy2yYYyhfXM1wWhftqgyzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.4.tgz", + "integrity": "sha512-r+rPQU1uaKDXWWWro6hDk3VrtBjmRyDz7UySTe9CU8HHG4eTXXLSHxasSwpOz/7FJZZuWBVcFERpjWszu04BbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.4.4", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.1.0", + "globals": "16.4.0", + "typescript-eslint": "^8.56.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.5.tgz", + "integrity": "sha512-yKEpcFuRPu/pTQL2VB8YKE2yFldrLlIsoWtJeBnAdqEhvu21OgkFT0C8njf/xYc2LSZGEr3K9JGjthHJxnVLvQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz", + "integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/linkifyjs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.4.0.tgz", + "integrity": "sha512-InTexvtbai+2c+dEVGcc6O6P2DMVIiwD9a/i4zFroazOIYLOS6FSVmAJ7kFeumnCMx6/gQqNAkbUvgMubV4+MA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.67.0.tgz", + "integrity": "sha512-z7kd21YNm/Gw3E7mTnq5dTuvV+yc+OEwA//oDENSG1SbZ/NVgKdjxALJFNdGoC0Ji7HaJR2Dha9ev7VUPxy9CQ==", + "license": "ISC", + "peerDependencies": { + "@types/react": "*", + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/magic-string": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.8.tgz", + "integrity": "sha512-dHbDC5V1g/EI8OLEtL4Iuwbu0fgzmyykxenn2KnlsX9XdhTXL3gWnfbbP+ab+oUcgV6CFpb8QGyQt/ZXA0UXig==", + "dev": true, + "license": "MIT", + "bin": { + "napi-patch": "lib/patch.js", + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.4.tgz", + "integrity": "sha512-Hu4pOw2x0RQ0TQabNWbpsNg/3+SZf+rLQwvfhBLnptv98GS4dcIVMlfEyn9jb2/ZJDSahhnUKEs0YrfarjGX3A==", + "license": "MIT", + "dependencies": { + "@next/env": "16.4.4", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.4.4", + "@next/swc-darwin-x64": "16.4.4", + "@next/swc-linux-arm64-gnu": "16.4.4", + "@next/swc-linux-arm64-musl": "16.4.4", + "@next/swc-linux-x64-gnu": "16.4.4", + "@next/swc-linux-x64-musl": "16.4.4", + "@next/swc-win32-arm64-msvc": "16.4.4", + "@next/swc-win32-x64-msvc": "16.4.4", + "sharp": "^0.35.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-releases": { + "version": "2.0.63", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.63.tgz", + "integrity": "sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prosemirror-changeset": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.6.tgz", + "integrity": "sha512-P0cY3DSLm4M2boUOCmHDyaID5o6HgWmb21Bvs9p8lLqfJjKBBEHG5Ma5qW5SCmwOYamtWOLf2k+yvt6077zlTg==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.4.tgz", + "integrity": "sha512-VmKxhcA6a+h5oPSYmfF1ZNeQqPbyYaIxqZsKqeYCyTlPtrjUgNaHL3P8vVaO19oqAV0zuaM/axkowZHrB1P3Tg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.1.tgz", + "integrity": "sha512-onlwnqKvgtFg6srZOmDOAhlAy1FIHQpOExORybX0S+DvJepE5tPALrB9iGet/2sqqqrelSnB10OoiOJqAvu2DA==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.12", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.12.tgz", + "integrity": "sha512-Ue2gTmXMa7EhpLNhC7J+h4+ykD8ha12K6rrZFFKKJHBForfIStw5gJ6Zrf1mqqAa9NmpmEB4wp9bKmA4eUVjWg==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.6.0.tgz", + "integrity": "sha512-G1srDtan+VqWn+qpmWFj/6j0YsZ5zMU+ZIlGVIW41a5JF2jAI3jzfMRWmSABmEH2Qoq1PmAG/o2mucMMdLOsLg==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.4.tgz", + "integrity": "sha512-2/WuPum3pn+O/Vi2vzpi8210ICUe2kIPXdLYjQ09Pv5DtT5bcKWMJhAKPBwvpPjpp5iOYUVwVEr0gOM1BY1Mgw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.9", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.9.tgz", + "integrity": "sha512-bKbfzeKZOu2H2JQU2TaJmYu6EBcor3VnGIR4jR31cbEt1ztUnfUB6bKGV1+fJzOJdrVWR+tRU0208zM3oQx4wA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.12", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", + "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.4.tgz", + "integrity": "sha512-PCJZTFFdG+sL3gvnP1Do9o6GbkvH2q0p04xSs3f7cSvmAeK+sttG+/7ncK1fdRwKFfC7n+wDDJaOMBSefkI9ow==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.5.tgz", + "integrity": "sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz", + "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.1", + "@typescript-eslint/parser": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.13.0.tgz", + "integrity": "sha512-mcPQo9Vm6kXd84d/WqMiYBi2CcA57QHBX/ev5PoUEb8b2oQzUvJ6xYsfseQblnCncgnVmCZGWiFwK7KkS5JZJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.7" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.13.0", + "@unrs/resolver-binding-android-arm64": "1.13.0", + "@unrs/resolver-binding-darwin-arm64": "1.13.0", + "@unrs/resolver-binding-darwin-x64": "1.13.0", + "@unrs/resolver-binding-freebsd-x64": "1.13.0", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.13.0", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.13.0", + "@unrs/resolver-binding-linux-arm64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-arm64-musl": "1.13.0", + "@unrs/resolver-binding-linux-loong64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-loong64-musl": "1.13.0", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-riscv64-musl": "1.13.0", + "@unrs/resolver-binding-linux-s390x-gnu": "1.13.0", + "@unrs/resolver-binding-linux-x64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-x64-musl": "1.13.0", + "@unrs/resolver-binding-openharmony-arm64": "1.13.0", + "@unrs/resolver-binding-win32-arm64-msvc": "1.13.0", + "@unrs/resolver-binding-win32-ia32-msvc": "1.13.0", + "@unrs/resolver-binding-win32-x64-msvc": "1.13.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.16.tgz", + "integrity": "sha512-Uv7e327ihlCfayfja3hDc0M64Upev1LWBKpRobdK4NyovDDPeeX4imWXc/wzC1yc6fkYPoAY/ncKVPAs2jrtQw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..491fcdb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@tailwindcss/typography": "^0.5.20", + "@tiptap/extension-blockquote": "^3.32.2", + "@tiptap/extension-bubble-menu": "^3.32.2", + "@tiptap/extension-color": "^3.32.2", + "@tiptap/extension-font-family": "^3.32.2", + "@tiptap/extension-text-style": "^3.32.2", + "@tiptap/extension-underline": "^3.32.2", + "@tiptap/react": "^3.32.2", + "@tiptap/starter-kit": "^3.32.2", + "axios": "^1.21.0", + "lucide-react": "^1.67.0", + "next": "16.4.4", + "react": "19.3.0", + "react-dom": "19.3.0", + "zustand": "^5.0.16" + }, + "devDependencies": { + "@tailwindcss/turbopack": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.4.4", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/frontend/public/file.svg b/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/diff-test/page.tsx b/frontend/src/app/diff-test/page.tsx new file mode 100644 index 0000000..9de13ba --- /dev/null +++ b/frontend/src/app/diff-test/page.tsx @@ -0,0 +1,5 @@ +import DiffTest from '@/components/test/DiffTest'; + +export default function DiffTestPage() { + return ; +} diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..af98450595e8b8efd9e505cddc5ed705b665a4d8 GIT binary patch literal 15086 zcmdU$XN*0PW5y#)gXL_h{3FkJ#X`oXpI?bB5#G)w~S7Qa+`W<1AB#X3C=?&UhvAO=|S+ zQpCR%R9`2h-z)7_x}fw(>6KDpfVd}*GI-!c2H9UDrKwS>=#NU@Ddj7@Q4*g7FEYrY z16}AW?v$zq+9+i!-B+T1{bG*9hmHqIUn`*-8^w`osM0?vVJi}rWb@IzKnYt#l~PpE zZq^t6O{HGFdQ!P^WzlxPHWGIHcd>D{}x={sTUg9&WW6602m zQnBH6>!yx}4jn2B7A%n2vuDesNt2{;dd zh7B@$^l0CFc@ZrPKrArsz@`@AQzbS#)CVarSbb!I8ty{PH zk}X}jv}9#v$)iV)Gb-#m>4qUO6)cT2PV3 zpI*Is$&Ww&XxZfC$&*s6RxM+zLWK&_s8J(BjQt6=fsGRsIez?@ix)2jvEROZTiUm8 zAGA}oYE|jjv7_)^<8Q|B8-jA4x_+h43HdwG`akQZ{rmR^u|IqEOoj~`7J{KpojS(< z>D#xjnZIoEL8L15HMT(z#WR80C=8eJ6m1l4LW@cu}s8OTj^Upsw@4?*W6SJES z?6iS9&)0pV`lp_$Z=3e*+h^9ZUK>rCG?5uIW|;4L`0(N0^@=YH0qjNg{kCh@PHx@0 zWueQ33l}2#^Q~LAHa;_LgYTK1p6(a3o6pAd+gv_56 zTeSG07MmF9|6)G^Q&#}`NMn_Um~~0isUI6)0h8ZU;`^Lf>f{vqND)`@P9?$9TK1fc;tCyQ6!+t!2dtUtke$_><;tw&q0@ECem?{wl!rS3|>b6OetkXT}@W?k_+rGrYh zl>Vi}m;6*0c#P>*l+@N$%>EBBFmQe{Fc#%OC9f_nmD)f~S=dohhj8vYl zv6`=x&=pVWsLX#+q8mig9W$TUxvt*}bzk7L-lNk`>Y+?Gm163T#zTyXvm=~)sb0N0 zaaweuJ6Zx`(zaEHG-%L3KK=Al6T>DZPhLQGq zy?Yqg@b-sP&7-d+WX~-}jDPy{>9TCuG9i8(A^wLgY}(XWZ-_f3WuJ2&?4j-5yH}Ph zSrTpU9$VNX@qMmWifEgC6yB+S|Ng&-_q*kY6P-PKR*oJ$D&4wui>NOkgH3F=P`Ve$ z3oFArk%&op&JP9oiCbR3eqHX}yJupw-hGKM`tJs8Gp0rz*J9ak*s!6TKY!j}B<@Qe z2xHHihfO=4d&@$equD1uh)vR!D_5jhvt}W(F>o+n#QfeP*=Ky4KYza26Uxue7xt`( zwGs1UFEoa2%CXJd6_q!V{lS9=%abQh(nAuU?8F#8U%^agA*f5vUU zO`A5Rzftd-H*XGp=j=}sgJvHomR>El^LJEU_xn@7oB=$3{5YoN^y$-PoVT`!UrwAj z(d>cRyeL=eJNUAXP0n?!TD8j9&dbY_&Ye4l$oA~n)9gP*+v9QJq3wP5KkQGLGDTj# zd};PI#*Q5uVjI0fh76HSn>LxVdoDY_;0u&tn{OaexyiTnfB*h{;fw|QLCcpf4;xpW zFkyme8_o^J(yQeMlzfjrrqcE2$dM!Fd_g$&SWBkEa^gaG!|Jt={ z=KFH#39++i(IT_Yx^(H%ko++GcKr1<|Ih~0rcI04uj!BQ`Z)V#=O2Y;=E9`s-!ShO zsSe!pFZPSf|L}>y>wkcu$o#Ju7{d8q*e4a4|Eapd)ji*PB>xlpq$2UZRgc#`wqyC< z*e8K8>Gg-|00V0nbj5maWeG|22l`3AlJ{NwJ|l|`ufMHXz7k^tYsg%g=)+?N8;M_9LtdS2KI3UcQZk+)cWFG|X4L^R?nKNgM zJp7#1#y{fx7Zcyn1ijH<*t+zGuIF{p8Z6OUCbF zJuqm{AfqFUdUSJ))2_z+s>OeFWelDY)Ji4$N2uccI^tDkzj4ezJD0s z6WNgVqu%*n4eJGq6%4dL{) { + return ( + + {children} + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..d1e9ee1 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { useAppStore } from '@/stores/appStore'; +import AuthScreen from '@/components/auth/AuthScreen'; +import LeftPanel from '@/components/sidebar/LeftPanel'; +import StoryBiblePanel from '@/components/sidebar/StoryBiblePanel'; +import { Editor } from '@/components/editor/Editor'; +import ModelSettingsPanel from '@/components/settings/ModelSettings'; + +export default function Home() { + const { isAuthenticated, isLoadingAuth } = useAppStore(); + + if (isLoadingAuth) { + return null; // AuthScreen показывает спиннер при checkAuth + } + + if (!isAuthenticated) { + return ; + } + + return ( +
+ {/* Left panel - Projects + Document Tree */} +
+ +
+ + {/* Middle panel - Story Bible */} +
+ +
+ + {/* Main content - Editor */} +
+ +
+ + {/* Model settings button */} + +
+ ); +} diff --git a/frontend/src/app/test/page.tsx b/frontend/src/app/test/page.tsx new file mode 100644 index 0000000..289525b --- /dev/null +++ b/frontend/src/app/test/page.tsx @@ -0,0 +1,11 @@ +import AITest from '@/components/test/AITest'; + +export default function TestPage() { + return ( +
+
+ +
+
+ ); +} diff --git a/frontend/src/components/auth/AuthScreen.tsx b/frontend/src/components/auth/AuthScreen.tsx new file mode 100644 index 0000000..ff951f5 --- /dev/null +++ b/frontend/src/components/auth/AuthScreen.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { useState } from 'react'; +import { useAppStore } from '@/stores/appStore'; +import { BookOpen, Loader2 } from 'lucide-react'; + +export default function AuthScreen() { + const { login, register, isLoadingAuth } = useAppStore(); + const [isLogin, setIsLogin] = useState(true); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsSubmitting(true); + try { + if (isLogin) { + await login(username, password); + } else { + await register(username, email, password); + } + } catch (err: any) { + setError(err.message || 'Ошибка авторизации'); + } finally { + setIsSubmitting(false); + } + }; + + if (isLoadingAuth) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ +

Writer IDE

+

Ваше пространство для творчества

+
+ +
+
+ + +
+ +
+
+ + setUsername(e.target.value)} + className="w-full p-2.5 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" + required + /> +
+ + {!isLogin && ( +
+ + setEmail(e.target.value)} + className="w-full p-2.5 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" + required + /> +
+ )} + +
+ + setPassword(e.target.value)} + className="w-full p-2.5 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" + required + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +

+ {isLogin ? 'Нет аккаунта? ' : 'Уже есть аккаунт? '} + +

+
+
+
+ ); +} diff --git a/frontend/src/components/editor/AIDiffView.tsx b/frontend/src/components/editor/AIDiffView.tsx new file mode 100644 index 0000000..26a14ae --- /dev/null +++ b/frontend/src/components/editor/AIDiffView.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { Check, X } from 'lucide-react'; + +interface AIDiffViewProps { + originalText: string; + aiText: string; + onAccept: () => void; + onReject: () => void; +} + +export default function AIDiffView({ originalText, aiText, onAccept, onReject }: AIDiffViewProps) { + return ( +
+
+ {/* Header */} +
+

Предпросмотр изменений

+
+ + +
+
+ + {/* Content */} +
+ {/* Original */} +
+
+
+

Оригинал

+
+
+ {originalText} +
+
+ + {/* AI Suggestion */} +
+
+
+

Предложение AI

+
+
+ {aiText} +
+
+
+ + {/* Footer hint */} +
+

+ Нажмите "Принять" для замены выделенного текста или "Отклонить" для отмены +

+
+
+
+ ); +} diff --git a/frontend/src/components/editor/AIMenu.tsx b/frontend/src/components/editor/AIMenu.tsx new file mode 100644 index 0000000..c1ce1be --- /dev/null +++ b/frontend/src/components/editor/AIMenu.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { useState } from 'react'; +import { Editor } from '@tiptap/react'; +import { BubbleMenu } from '@tiptap/react/menus'; +import { Sparkles, RefreshCw, Expand, Minimize, MessageSquare, PlusCircle } from 'lucide-react'; + +interface AIMenuProps { + editor: Editor | null; + onAction: (action: string, selectedText: string) => void; +} + +type AIAction = 'rewrite' | 'expand' | 'simplify' | 'custom' | 'add_to_bible'; + +export default function AIMenu({ editor, onAction }: AIMenuProps) { + const [showDropdown, setShowDropdown] = useState(false); + + if (!editor) return null; + + const handleAction = (action: AIAction) => { + const { from, to } = editor.state.selection; + const selectedText = editor.state.doc.textBetween(from, to, ' '); + + if (!selectedText.trim()) { + return; + } + + onAction(action, selectedText); + setShowDropdown(false); + }; + + return ( + { + return from !== to && !editor.isActive('image'); + }} + > +
+ {/* Кнопка вызова AI меню */} + + + {/* Dropdown с действиями */} + {showDropdown && ( +
+
+ + + + + + +
+ + + + +
+
+ )} +
+ + ); +} diff --git a/frontend/src/components/editor/AddToBibleModal.tsx b/frontend/src/components/editor/AddToBibleModal.tsx new file mode 100644 index 0000000..c958adf --- /dev/null +++ b/frontend/src/components/editor/AddToBibleModal.tsx @@ -0,0 +1,237 @@ +'use client'; + +import { useState } from 'react'; +import { X, User, MapPin, Calendar, Check } from 'lucide-react'; + +interface ExtractedEntity { + name: string; + description: string; + traits?: string[]; + appearance?: string; + role?: string; + atmosphere?: string; + type?: string; + importance?: string; + title?: string; +} + +interface AddToBibleModalProps { + extractedEntities: { + new_characters: ExtractedEntity[]; + new_locations: ExtractedEntity[]; + new_events: ExtractedEntity[]; + }; + onConfirm: (selected: { + characters: ExtractedEntity[]; + locations: ExtractedEntity[]; + events: ExtractedEntity[]; + }) => void; + onClose: () => void; +} + +export default function AddToBibleModal({ extractedEntities, onConfirm, onClose }: AddToBibleModalProps) { + const [selectedChars, setSelectedChars] = useState>( + new Set(extractedEntities.new_characters.map((_, i) => i)) + ); + const [selectedLocs, setSelectedLocs] = useState>( + new Set(extractedEntities.new_locations.map((_, i) => i)) + ); + const [selectedEvents, setSelectedEvents] = useState>( + new Set(extractedEntities.new_events.map((_, i) => i)) + ); + + const handleConfirm = () => { + onConfirm({ + characters: extractedEntities.new_characters.filter((_, i) => selectedChars.has(i)), + locations: extractedEntities.new_locations.filter((_, i) => selectedLocs.has(i)), + events: extractedEntities.new_events.filter((_, i) => selectedEvents.has(i)), + }); + }; + + const totalSelected = selectedChars.size + selectedLocs.size + selectedEvents.size; + + return ( +
+
+ {/* Header */} +
+

Добавить в Story Bible

+ +
+ + {/* Content */} +
+ {/* Characters */} + {extractedEntities.new_characters.length > 0 && ( +
+

+ + Персонажи ({extractedEntities.new_characters.length}) +

+
+ {extractedEntities.new_characters.map((char, idx) => ( + + ))} +
+
+ )} + + {/* Locations */} + {extractedEntities.new_locations.length > 0 && ( +
+

+ + Локации ({extractedEntities.new_locations.length}) +

+
+ {extractedEntities.new_locations.map((loc, idx) => ( + + ))} +
+
+ )} + + {/* Events */} + {extractedEntities.new_events.length > 0 && ( +
+

+ + События ({extractedEntities.new_events.length}) +

+
+ {extractedEntities.new_events.map((event, idx) => ( + + ))} +
+
+ )} + + {/* Empty state */} + {extractedEntities.new_characters.length === 0 && + extractedEntities.new_locations.length === 0 && + extractedEntities.new_events.length === 0 && ( +
+

Новых сущностей не обнаружено

+

Попробуйте выделить другой текст

+
+ )} +
+ + {/* Footer */} +
+

+ Выбрано: {totalSelected} {totalSelected === 1 ? 'сущность' : totalSelected <= 4 ? 'сущности' : 'сущностей'} +

+
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/components/editor/BubbleMenuWrapper.tsx b/frontend/src/components/editor/BubbleMenuWrapper.tsx new file mode 100644 index 0000000..de2b1a9 --- /dev/null +++ b/frontend/src/components/editor/BubbleMenuWrapper.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { useEditor } from '@tiptap/react'; +import { BubbleMenu } from '@tiptap/extension-bubble-menu'; +import { Editor } from '@tiptap/react'; +import { ReactNode, useEffect, useRef } from 'react'; + +interface BubbleMenuWrapperProps { + editor: Editor | null; + children: ReactNode; + shouldShow?: (props: { editor: Editor; from: number; to: number }) => boolean; + tippyOptions?: { + duration?: number; + moveTransition?: string; + }; +} + +export function BubbleMenuWrapper({ + editor, + children, + shouldShow, + tippyOptions, +}: BubbleMenuWrapperProps) { + const bubbleMenuRef = useRef(null); + + // Создаем BubbleMenu extension + const bubbleMenuExtension = BubbleMenu.configure({ + shouldShow: shouldShow || ({ from, to }) => from !== to, + tippyOptions: { + duration: 100, + moveTransition: 'transform 0.15s ease-out', + ...tippyOptions, + }, + }); + + // Добавляем extension к редактору (если еще не добавлен) + useEffect(() => { + if (!editor) return; + + const hasBubbleMenu = editor.options.extensions.some( + (ext: { name: string }) => ext.name === 'bubbleMenu' + ); + + if (!hasBubbleMenu) { + editor.extendGlobalExtension?.(bubbleMenuExtension); + } + }, [editor, bubbleMenuExtension]); + + if (!editor) return null; + + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/editor/Editor.tsx b/frontend/src/components/editor/Editor.tsx new file mode 100644 index 0000000..76e0d84 --- /dev/null +++ b/frontend/src/components/editor/Editor.tsx @@ -0,0 +1,480 @@ +'use client'; + +import { useEditor, EditorContent } from '@tiptap/react'; +import type { Editor as TiptapEditor } from '@tiptap/core'; +import { Toolbar } from './Toolbar'; +import AIMenu from './AIMenu'; +import AddToBibleModal from './AddToBibleModal'; +import StarterKit from '@tiptap/starter-kit'; +import { TextStyle } from '@tiptap/extension-text-style'; +import { Color } from '@tiptap/extension-color'; +// StarterKit уже включает underline и blockquote, поэтому не добавляем их отдельно +import { FontFamily } from '@tiptap/extension-font-family'; +import { FontSize } from '@/lib/tiptap-extensions/FontSize'; +import { useEffect, useRef, useCallback, useState } from 'react'; +import { useAppStore } from '@/stores/appStore'; +import { generateText, autoUpdateBible, extractEntities } from '@/lib/ai-api'; +import AIDiffView from './AIDiffView'; +import { Loader2, AlertCircle } from 'lucide-react'; + +// Расширяем Window для глобального callback +declare global { + interface Window { + saveCurrentEditorContent?: () => Promise; + } +} + +interface ExtractedEntity { + name: string; + description: string; + traits?: string[]; + appearance?: string; + role?: string; + atmosphere?: string; + type?: string; + importance?: string; + title?: string; +} + +interface ExtractedEntitiesData { + new_characters: ExtractedEntity[]; + new_locations: ExtractedEntity[]; + new_events: ExtractedEntity[]; +} + +export function Editor() { + const { + documents, + activeDocumentId, + activeProjectId, + updateDocumentContent, + loadStoryBible, + autoUpdateBible: autoUpdateEnabled, + isSaving + } = useAppStore(); + + // Находим активный документ + const activeDocument = documents.find(d => d.id === activeDocumentId); + const debounceTimerRef = useRef(null); + const bibleUpdateTimeoutRef = useRef(null); + const editorRef = useRef(null); + + // AI workflow state + const [selectedText, setSelectedText] = useState(''); + const [aiGeneratedText, setAiGeneratedText] = useState(''); + const [showDiffView, setShowDiffView] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [selectionRange, setSelectionRange] = useState<{ from: number; to: number } | null>(null); + + // State для модального окна добавления в Story Bible + const [showAddToBibleModal, setShowAddToBibleModal] = useState(false); + const [extractedEntities, setExtractedEntities] = useState<{ + new_characters: ExtractedEntity[]; + new_locations: ExtractedEntity[]; + new_events: ExtractedEntity[]; + } | null>(null); + + // Функция для извлечения чистого текста из HTML + const getPlainText = (html: string): string => { + const div = document.createElement('div'); + div.innerHTML = html; + return div.textContent || div.innerText || ''; + }; + + // Функция для debounced обновления контента с сохранением на backend + const debouncedUpdate = useCallback( + (html: string) => { + if (!activeProjectId || !activeDocumentId) return; + + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + debounceTimerRef.current = setTimeout(async () => { + await updateDocumentContent(activeProjectId, activeDocumentId, html); + + // Автообновление Story Bible (с задержкой после сохранения) + if (autoUpdateEnabled) { + if (bibleUpdateTimeoutRef.current) { + clearTimeout(bibleUpdateTimeoutRef.current); + } + + bibleUpdateTimeoutRef.current = setTimeout(async () => { + try { + const plainText = getPlainText(html); + if (plainText.trim().length > 50) { + const result = await autoUpdateBible( + activeProjectId, + plainText, + autoUpdateEnabled + ); + + if (result.updated) { + console.log('Story Bible updated:', result.message); + // Перезагружаем Story Bible в store + loadStoryBible(activeProjectId); + } + } + } catch (error) { + console.error('Auto-update Bible error:', error); + } + }, 3000); // Задержка 3 секунды после сохранения + } + }, 1000); // Задержка 1 секунда для сохранения + }, + [activeProjectId, activeDocumentId, updateDocumentContent, autoUpdateEnabled, loadStoryBible] + ); + + // Функция для немедленного сохранения текущего контента + const saveCurrentContent = useCallback(async (ed: TiptapEditor | null) => { + const currentEditor = ed || editorRef.current; + if (currentEditor && activeProjectId && activeDocumentId) { + const currentContent = currentEditor.getHTML(); + + // Очищаем таймер debounce + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + debounceTimerRef.current = null; + } + + // Немедленно сохраняем + try { + await updateDocumentContent(activeProjectId, activeDocumentId, currentContent); + console.log('[Editor] Content saved before switching:', currentContent.substring(0, 100)); + } catch (error) { + console.error('[Editor] Failed to save before switching:', error); + } + } + }, [activeProjectId, activeDocumentId, updateDocumentContent]); + + // Очистка таймеров при размонтировании + useEffect(() => { + return () => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + if (bibleUpdateTimeoutRef.current) { + clearTimeout(bibleUpdateTimeoutRef.current); + } + }; + }, []); + + // Обработчик действий AI + const handleAIAction = async (action: string, text: string) => { + if (!editor) return; + + const { from, to } = editor.state.selection; + setSelectedText(text); + setSelectionRange({ from, to }); + + // Специальная обработка для add_to_bible + if (action === 'add_to_bible') { + setIsLoading(true); + setError(null); + + try { + const result = await extractEntities(text); + setExtractedEntities(result.entities); + setShowAddToBibleModal(true); + } catch (err) { + setError(err instanceof Error ? err.message : 'Ошибка при извлечении сущностей'); + console.error('Extract entities error:', err); + } finally { + setIsLoading(false); + } + return; + } + + setIsLoading(true); + setError(null); + + try { + let prompt = ''; + switch (action) { + case 'rewrite': + prompt = `Перепиши следующий текст, сделай его более живым, образным и кинематографичным. Сохрани основной смысл, но улучши стиль:\n\n${text}`; + break; + case 'expand': + prompt = `Расширь следующий текст, добавь больше деталей, описаний, сенсорных деталей (запахи, звуки, ощущения). Увеличь объем примерно в 1.5 раза:\n\n${text}`; + break; + case 'simplify': + prompt = `Упрости следующий текст, сделай его более кратким и понятным. Убери лишние слова и сложные конструкции, сохрани основной смысл:\n\n${text}`; + break; + case 'custom': + prompt = `Улучши следующий текст:\n\n${text}`; + break; + default: + prompt = `Улучши следующий текст:\n\n${text}`; + } + + const generatedText = await generateText({ + prompt, + preset: 'creative_draft', + taskType: 'generation', + selected_text: text, + }); + + setAiGeneratedText(generatedText); + setShowDiffView(true); + } catch (err) { + setError(err instanceof Error ? err.message : 'Произошла ошибка при генерации'); + console.error('AI generation error:', err); + } finally { + setIsLoading(false); + } + }; + + const editor = useEditor({ + extensions: [ + StarterKit, // включает underline и blockquote + TextStyle, + Color, + FontFamily, + FontSize, + ], + content: activeDocument?.content || '', + onUpdate: ({ editor: ed }) => { + // Sync editor ref + editorRef.current = ed; + + if (activeProjectId && activeDocumentId) { + const content = ed.getHTML(); + console.log('[Editor] onUpdate - content length:', content.length); + console.log('[Editor] onUpdate - content preview:', content.substring(0, 150)); + debouncedUpdate(content); + } + }, + editorProps: { + attributes: { + class: 'prose prose-invert max-w-none focus:outline-none font-serif text-lg leading-relaxed', + }, + }, + immediatelyRender: false, + onCreate: ({ editor: ed }) => { + editorRef.current = ed; + }, + }); + + // Регистрируем глобальный callback для сохранения + useEffect(() => { + if (editor) { + editorRef.current = editor; + // Создаём обёртку без параметров для глобального доступа + window.saveCurrentEditorContent = () => saveCurrentContent(editor); + + return () => { + delete window.saveCurrentEditorContent; + }; + } + }, [editor, saveCurrentContent]); + + // Синхронизация редактора при смене активного документа + useEffect(() => { + if (!editor || !activeDocument) return; + + console.log('=== Loading document ==='); + console.log('[Editor] Document ID:', activeDocument.id); + console.log('[Editor] Document title:', activeDocument.title); + console.log('[Editor] Content length:', activeDocument.content?.length || 0); + console.log('[Editor] Content preview:', activeDocument.content?.substring(0, 200)); + + // Проверяем, что контент действительно изменился + const currentContent = editor.getHTML(); + const newContent = activeDocument.content || ''; + + // Загружаем только если контент действительно другой + if (currentContent !== newContent) { + console.log('[Editor] Content differs, loading new content'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (editor.commands as any).setContent(newContent, false, { preserveWhitespace: 'full' }); + } else { + console.log('[Editor] Content is the same, skipping reload'); + } + }, [activeDocumentId, editor, activeDocument]); + + // Если нет активного документа или это не сцена + if (!activeDocument || activeDocument.type !== 'scene') { + return ( +
+
+

Выберите сцену для редактирования

+

Кликните на сцену в дереве документов

+
+
+ ); + } + + // Обработчики для AIDiffView + const handleAcceptChanges = () => { + if (editor && selectionRange && aiGeneratedText) { + editor + .chain() + .focus() + .setTextSelection({ from: selectionRange.from, to: selectionRange.to }) + .deleteRange({ from: selectionRange.from, to: selectionRange.to }) + .insertContent(aiGeneratedText) + .run(); + } + setShowDiffView(false); + setAiGeneratedText(''); + setSelectedText(''); + setSelectionRange(null); + }; + + const handleRejectChanges = () => { + setShowDiffView(false); + setAiGeneratedText(''); + setSelectedText(''); + setSelectionRange(null); + }; + + // Обработчик подтверждения добавления в Story Bible + const handleAddToBibleConfirm = async (selected: { + characters: ExtractedEntity[]; + locations: ExtractedEntity[]; + events: ExtractedEntity[]; + }) => { + // Получаем данные из store + const state = useAppStore.getState(); + const { activeProjectId, storyBible, updateStoryBible } = state; + + if (!activeProjectId || !storyBible) return; + + try { + // Создаем копию текущей Bible + const updatedBible = JSON.parse(JSON.stringify(storyBible)); + + // Добавляем выбранные персонажи + selected.characters.forEach(char => { + updatedBible.characters.push({ + id: crypto.randomUUID(), + name: char.name, + description: char.description || '', + traits: char.traits || [], + appearance: char.appearance || '', + role: char.role || 'supporting' + }); + }); + + // Добавляем выбранные локации + selected.locations.forEach(loc => { + updatedBible.locations.push({ + id: crypto.randomUUID(), + name: loc.name, + description: loc.description || '', + atmosphere: loc.atmosphere || '', + type: loc.type || 'other' + }); + }); + + // Добавляем выбранные события + selected.events.forEach(event => { + updatedBible.events.push({ + id: crypto.randomUUID(), + title: event.title || event.name || '', + description: event.description || '', + importance: event.importance || 'minor' + }); + }); + + // Сохраняем обновленную Bible через API + await updateStoryBible(activeProjectId, updatedBible); + + // Закрываем модальное окно + setShowAddToBibleModal(false); + setExtractedEntities(null); + + // Логируем результат + const totalAdded = selected.characters.length + selected.locations.length + selected.events.length; + console.log(`Добавлено в Story Bible: ${totalAdded} сущностей`); + } catch (error) { + console.error('Failed to add to Bible:', error); + setError('Ошибка при добавлении в Story Bible'); + } + }; + + // Проверка на наличие активного проекта + if (!activeProjectId) { + return ( +
+
+

Выберите проект

+

Или создайте новый в левой панели

+
+
+ ); + } + + // Проверка на наличие активного документа + if (!activeDocument) { + return ( +
+
+

Нет документов в проекте

+

Создайте документ через Story Bible или добавьте его позже

+
+
+ ); + } + + return ( +
+ {/* Индикатор сохранения */} +
+ + {isSaving ? 'Сохранение...' : 'Сохранено'} + +
+ + {/* Скроллируемая область редактора */} +
+
+ +
+
+ + + {/* Diff view для предпросмотра AI изменений */} + {showDiffView && ( + + )} + + {/* Модальное окно добавления в Story Bible */} + {showAddToBibleModal && extractedEntities && ( + { + setShowAddToBibleModal(false); + setExtractedEntities(null); + }} + /> + )} + + {/* Индикатор загрузки */} + {isLoading && ( +
+ + AI генерирует... +
+ )} + + {/* Индикатор ошибки */} + {error && ( +
+ + {error} + +
+ )} +
+ ); +} diff --git a/frontend/src/components/editor/ExportModal.tsx b/frontend/src/components/editor/ExportModal.tsx new file mode 100644 index 0000000..7b71ad0 --- /dev/null +++ b/frontend/src/components/editor/ExportModal.tsx @@ -0,0 +1,127 @@ +'use client'; + +import { useState } from 'react'; +import { X, Download, Loader2, FileText, BookOpen } from 'lucide-react'; +import { exportDocument } from '@/lib/api'; + +interface ExportModalProps { + projectId: string; + documentId: string; + documentTitle: string; + documentType: 'book' | 'chapter'; + onClose: () => void; +} + +export default function ExportModal({ projectId, documentId, documentTitle, documentType, onClose }: ExportModalProps) { + const [selectedFormat, setSelectedFormat] = useState<'epub' | 'fb2' | 'pdf' | 'docx'>('epub'); + const [isExporting, setIsExporting] = useState(false); + const [error, setError] = useState(null); + + const handleExport = async () => { + setIsExporting(true); + setError(null); + try { + await exportDocument(projectId, documentId, selectedFormat); + onClose(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Ошибка при экспорте'); + } finally { + setIsExporting(false); + } + }; + + const formats = [ + { id: 'epub' as const, name: 'EPUB', desc: 'Стандарт для электронных книг (Apple Books, Kindle)', icon: BookOpen }, + { id: 'fb2' as const, name: 'FB2', desc: 'Популярный формат в СНГ (FBReader, LitRes)', icon: FileText }, + { id: 'pdf' as const, name: 'PDF', desc: 'Для печати и фиксированного форматирования', icon: FileText }, + { id: 'docx' as const, name: 'DOCX', desc: 'Для передачи на редактуру (Word, LibreOffice)', icon: FileText }, + ]; + + return ( +
+
+ {/* Header */} +
+

Экспорт: {documentTitle}

+ +
+ + {/* Content */} +
+

+ Выберите формат для экспорта {documentType === 'book' ? 'всей книги' : 'этой главы и всех вложенных сцен'}. +

+ +
+ {formats.map((fmt) => { + const Icon = fmt.icon; + return ( + + ); + })} +
+ + {error && ( +
+ {error} +
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/editor/GenerateImageModal.tsx b/frontend/src/components/editor/GenerateImageModal.tsx new file mode 100644 index 0000000..25f85f4 --- /dev/null +++ b/frontend/src/components/editor/GenerateImageModal.tsx @@ -0,0 +1,203 @@ +'use client'; + +import { useState } from 'react'; +import { X, Loader2, Sparkles, Download } from 'lucide-react'; +import { generateImage, GenerateImageResponse } from '@/lib/api'; + +interface GenerateImageModalProps { + projectId: string; + documentId: string; + documentTitle: string; + documentType: 'book' | 'chapter'; + onClose: () => void; +} + +export default function GenerateImageModal({ + projectId, + documentId, + documentTitle, + documentType, + onClose +}: GenerateImageModalProps) { + const [prompt, setPrompt] = useState(''); + const [isGenerating, setIsGenerating] = useState(false); + const [generatedImage, setGeneratedImage] = useState(null); + const [error, setError] = useState(null); + + const suggestedPrompts = documentType === 'book' + ? [ + 'Epic fantasy book cover, magical castle in the clouds, dramatic lighting, digital art', + 'Mystery novel cover, dark city street at night, fog, noir style', + 'Romance book cover, couple silhouette at sunset, warm colors, artistic' + ] + : [ + 'Detailed illustration of a medieval tavern interior, warm lighting, fantasy style', + 'Character portrait, young adventurer with a sword, determined expression, digital painting', + 'Landscape illustration, mysterious forest path, magical atmosphere, concept art' + ]; + + const handleGenerate = async () => { + if (!prompt.trim()) return; + + setIsGenerating(true); + setError(null); + setGeneratedImage(null); + + try { + const result = await generateImage({ + prompt, + project_id: projectId, + document_id: documentId, + document_type: documentType, + }); + setGeneratedImage(result); + } catch (err) { + setError(err instanceof Error ? err.message : 'Ошибка при генерации изображения'); + console.error('Generate image error:', err); + } finally { + setIsGenerating(false); + } + }; + + const handleDownload = () => { + if (!generatedImage) return; + + // Скачиваем через blob из base64 данных + const byteString = atob(generatedImage.data_url.split(',')[1]); + const mimeString = generatedImage.data_url.split(',')[0].split(':')[1].split(';')[0]; + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + const blob = new Blob([ab], { type: mimeString }); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = `${documentTitle.replace(/[^a-z0-9]/gi, '_')}_${generatedImage.filename}`; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + }; + + return ( +
+
+ {/* Header */} +
+

+ Генерация изображения: {documentTitle} +

+ +
+ + {/* Content */} +
+ {/* Prompt input */} +
+ +