From 86ab6bc8fd4d3a1d133f34f8a32b06f6959ff941 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Wed, 13 May 2026 10:12:31 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=85=D0=B0=D0=B1=20=D0=B4=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавлен React/Vite-лендинг с карточками документаций - добавлена генерация корневого llms.txt из конфига документов - добавлена сборка SLM Design через VitePress - добавлены Dockerfile, Caddyfile и Gitea CI/CD - настроены контекстные Link headers для llms.txt --- .dockerignore | 12 + .gitea/workflows/ci.yml | 126 + .gitignore | 29 + Caddyfile | 49 + Dockerfile | 14 + README.md | 73 + canons/slm-design/architecture/index.md | 114 + canons/slm-design/architecture/layers.md | 254 + canons/slm-design/architecture/modules.md | 215 + canons/slm-design/architecture/monorepo.md | 235 + canons/slm-design/architecture/segments.md | 181 + .../examples/react/composition-provider.md | 249 + .../examples/react/factory-composition.md | 52 + canons/slm-design/examples/react/factory.md | 114 + canons/style-guide/DEVELOP.md | 103 + canons/style-guide/MAP.md | 59 + canons/style-guide/applied/aliases.md | 77 + canons/style-guide/applied/biome.md | 114 + canons/style-guide/applied/component.md | 165 + .../applied/creating-project/from-template.md | 51 + .../applied/creating-project/manual.md | 90 + .../applied/creating-project/nextjs.md | 112 + .../data-fetch/business-composition.md | 121 + .../applied/data-fetch/client-get-hook.md | 88 + .../data-fetch/client-hooks-initial-data.md | 117 + .../style-guide/applied/data-fetch/index.md | 100 + .../data-fetch/parallel-server-requests.md | 94 + .../applied/data-fetch/pass-promise-down.md | 64 + .../applied/data-fetch/server-await.md | 88 + canons/style-guide/applied/fonts.md | 128 + canons/style-guide/applied/images.md | 95 + canons/style-guide/applied/localization.md | 81 + canons/style-guide/applied/module.md | 156 + canons/style-guide/applied/page-level.md | 186 + canons/style-guide/applied/postcss.md | 70 + .../style-guide/applied/project-structure.md | 101 + .../style-guide/applied/rest-client/index.md | 50 + .../applied/rest-client/setup/auto.md | 272 + .../applied/rest-client/setup/hooks.md | 313 + .../applied/rest-client/setup/index.md | 88 + .../applied/rest-client/setup/manual.md | 198 + .../style-guide/applied/rest-client/usage.md | 21 + canons/style-guide/applied/stores.md | 0 .../applied/styles/styles-setup.md | 176 + .../applied/styles/styles-usage.md | 271 + .../applied/svg-sprites/svg-sprites-intro.md | 31 + .../applied/svg-sprites/svg-sprites-setup.md | 132 + .../applied/svg-sprites/svg-sprites-usage.md | 56 + .../applied/templates/templates-create.md | 97 + .../applied/templates/templates-intro.md | 32 + .../applied/templates/templates-setup.md | 44 + .../applied/templates/templates-usage.md | 45 + canons/style-guide/applied/vscode.md | 88 + .../style-guide/basics/architecture/index.md | 109 + .../style-guide/basics/architecture/layers.md | 254 + .../basics/architecture/modules.md | 289 + .../basics/architecture/segments.md | 181 + canons/style-guide/basics/code-style.md | 153 + canons/style-guide/basics/documentation.md | 134 + canons/style-guide/basics/naming.md | 146 + canons/style-guide/basics/tech-stack.md | 42 + canons/style-guide/basics/typing.md | 62 + canons/style-guide/index.md | 79 + canons/style-guide/slm-design/VERSION | 2 + .../slm-design/architecture/index.md | 114 + .../slm-design/architecture/layers.md | 254 + .../slm-design/architecture/modules.md | 213 + .../slm-design/architecture/monorepo.md | 235 + .../slm-design/architecture/segments.md | 181 + .../examples/react/composition-provider.md | 249 + .../examples/react/factory-composition.md | 52 + .../slm-design/examples/react/factory.md | 114 + canons/style-guide/workflow.md | 8 + .../deps_temp_f6246001/chunk-BKAC4ZFU.js | 13035 ++++++++++++++++ .../deps_temp_f6246001/chunk-BKAC4ZFU.js.map | 7 + .../cache/deps_temp_f6246001/package.json | 3 + .../vitepress___@vue_devtools-api.js | 4505 ++++++ .../vitepress___@vue_devtools-api.js.map | 7 + .../vitepress___@vueuse_core.js | 9731 ++++++++++++ .../vitepress___@vueuse_core.js.map | 7 + .../cache/deps_temp_f6246001/vue.js | 347 + .../cache/deps_temp_f6246001/vue.js.map | 7 + docs/slm-design/.vitepress/config.ts | 19 + docs/slm-design/docs.config.ts | 66 + eslint.config.js | 22 + index.html | 14 + package-lock.json | 6230 ++++++++ package.json | 36 + public/favicon.svg | 1 + public/icons.svg | 24 + scripts/docs/prepare.ts | 85 + scripts/site/generate-artifacts.ts | 65 + src/App.css | 465 + src/App.tsx | 290 + src/assets/hero.png | Bin 0 -> 13057 bytes src/assets/react.svg | 1 + src/assets/vite.svg | 1 + src/config/docs.config.ts | 67 + src/index.css | 83 + src/main.tsx | 10 + tsconfig.app.json | 25 + tsconfig.json | 7 + tsconfig.node.json | 24 + vite.config.ts | 63 + 104 files changed, 44009 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Caddyfile create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 canons/slm-design/architecture/index.md create mode 100644 canons/slm-design/architecture/layers.md create mode 100644 canons/slm-design/architecture/modules.md create mode 100644 canons/slm-design/architecture/monorepo.md create mode 100644 canons/slm-design/architecture/segments.md create mode 100644 canons/slm-design/examples/react/composition-provider.md create mode 100644 canons/slm-design/examples/react/factory-composition.md create mode 100644 canons/slm-design/examples/react/factory.md create mode 100644 canons/style-guide/DEVELOP.md create mode 100644 canons/style-guide/MAP.md create mode 100644 canons/style-guide/applied/aliases.md create mode 100644 canons/style-guide/applied/biome.md create mode 100644 canons/style-guide/applied/component.md create mode 100644 canons/style-guide/applied/creating-project/from-template.md create mode 100644 canons/style-guide/applied/creating-project/manual.md create mode 100644 canons/style-guide/applied/creating-project/nextjs.md create mode 100644 canons/style-guide/applied/data-fetch/business-composition.md create mode 100644 canons/style-guide/applied/data-fetch/client-get-hook.md create mode 100644 canons/style-guide/applied/data-fetch/client-hooks-initial-data.md create mode 100644 canons/style-guide/applied/data-fetch/index.md create mode 100644 canons/style-guide/applied/data-fetch/parallel-server-requests.md create mode 100644 canons/style-guide/applied/data-fetch/pass-promise-down.md create mode 100644 canons/style-guide/applied/data-fetch/server-await.md create mode 100644 canons/style-guide/applied/fonts.md create mode 100644 canons/style-guide/applied/images.md create mode 100644 canons/style-guide/applied/localization.md create mode 100644 canons/style-guide/applied/module.md create mode 100644 canons/style-guide/applied/page-level.md create mode 100644 canons/style-guide/applied/postcss.md create mode 100644 canons/style-guide/applied/project-structure.md create mode 100644 canons/style-guide/applied/rest-client/index.md create mode 100644 canons/style-guide/applied/rest-client/setup/auto.md create mode 100644 canons/style-guide/applied/rest-client/setup/hooks.md create mode 100644 canons/style-guide/applied/rest-client/setup/index.md create mode 100644 canons/style-guide/applied/rest-client/setup/manual.md create mode 100644 canons/style-guide/applied/rest-client/usage.md create mode 100644 canons/style-guide/applied/stores.md create mode 100644 canons/style-guide/applied/styles/styles-setup.md create mode 100644 canons/style-guide/applied/styles/styles-usage.md create mode 100644 canons/style-guide/applied/svg-sprites/svg-sprites-intro.md create mode 100644 canons/style-guide/applied/svg-sprites/svg-sprites-setup.md create mode 100644 canons/style-guide/applied/svg-sprites/svg-sprites-usage.md create mode 100644 canons/style-guide/applied/templates/templates-create.md create mode 100644 canons/style-guide/applied/templates/templates-intro.md create mode 100644 canons/style-guide/applied/templates/templates-setup.md create mode 100644 canons/style-guide/applied/templates/templates-usage.md create mode 100644 canons/style-guide/applied/vscode.md create mode 100644 canons/style-guide/basics/architecture/index.md create mode 100644 canons/style-guide/basics/architecture/layers.md create mode 100644 canons/style-guide/basics/architecture/modules.md create mode 100644 canons/style-guide/basics/architecture/segments.md create mode 100644 canons/style-guide/basics/code-style.md create mode 100644 canons/style-guide/basics/documentation.md create mode 100644 canons/style-guide/basics/naming.md create mode 100644 canons/style-guide/basics/tech-stack.md create mode 100644 canons/style-guide/basics/typing.md create mode 100644 canons/style-guide/index.md create mode 100644 canons/style-guide/slm-design/VERSION create mode 100644 canons/style-guide/slm-design/architecture/index.md create mode 100644 canons/style-guide/slm-design/architecture/layers.md create mode 100644 canons/style-guide/slm-design/architecture/modules.md create mode 100644 canons/style-guide/slm-design/architecture/monorepo.md create mode 100644 canons/style-guide/slm-design/architecture/segments.md create mode 100644 canons/style-guide/slm-design/examples/react/composition-provider.md create mode 100644 canons/style-guide/slm-design/examples/react/factory-composition.md create mode 100644 canons/style-guide/slm-design/examples/react/factory.md create mode 100644 canons/style-guide/workflow.md create mode 100644 docs/slm-design/.vitepress/cache/deps_temp_f6246001/chunk-BKAC4ZFU.js create mode 100644 docs/slm-design/.vitepress/cache/deps_temp_f6246001/chunk-BKAC4ZFU.js.map create mode 100644 docs/slm-design/.vitepress/cache/deps_temp_f6246001/package.json create mode 100644 docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vue_devtools-api.js create mode 100644 docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vue_devtools-api.js.map create mode 100644 docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vueuse_core.js create mode 100644 docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vueuse_core.js.map create mode 100644 docs/slm-design/.vitepress/cache/deps_temp_f6246001/vue.js create mode 100644 docs/slm-design/.vitepress/cache/deps_temp_f6246001/vue.js.map create mode 100644 docs/slm-design/.vitepress/config.ts create mode 100644 docs/slm-design/docs.config.ts create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 public/icons.svg create mode 100644 scripts/docs/prepare.ts create mode 100644 scripts/site/generate-artifacts.ts create mode 100644 src/App.css create mode 100644 src/App.tsx create mode 100644 src/assets/hero.png create mode 100644 src/assets/react.svg create mode 100644 src/assets/vite.svg create mode 100644 src/config/docs.config.ts create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ae8aa6d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.git +.gitea +.claude +dist +docs/*/.vitepress/cache +docs/*/.vitepress/dist +docs/*/content +public/slm-design +*.log +.DS_Store +.env* diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..c1094a0 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,126 @@ +name: CI/CD Pipeline + +on: + push: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Установка зависимостей + run: npm ci + + - name: Сборка документации SLM Design + run: npm run docs:build:slm-design + + - name: Генерация корневых артефактов + run: npm run site:generate + + - name: Сборка лендинга + run: npm run build + + docker: + runs-on: ubuntu-latest + needs: build + if: "github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, '[skip ci]')" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: master + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Setup variables + run: | + DOCKER_REGISTRY=$(echo "${{ gitea.server_url }}" | sed 's|https://||') + echo "DOCKER_REGISTRY=$DOCKER_REGISTRY" >> $GITHUB_ENV + REGISTRY_IMAGE="$DOCKER_REGISTRY/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" + echo "REGISTRY_IMAGE=$REGISTRY_IMAGE" >> $GITHUB_ENV + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=ref,event=branch + type=sha,prefix= + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false + sbom: false + + deploy: + runs-on: ubuntu-latest + needs: docker + if: "github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, '[skip ci]')" + steps: + - name: Setup variables + run: | + DOCKER_REGISTRY=$(echo "${{ gitea.server_url }}" | sed 's|https://||') + echo "DOCKER_REGISTRY=$DOCKER_REGISTRY" >> $GITHUB_ENV + REGISTRY_IMAGE="$DOCKER_REGISTRY/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" + echo "REGISTRY_IMAGE=$REGISTRY_IMAGE" >> $GITHUB_ENV + + - name: Настройка SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H 188.225.47.78 >> ~/.ssh/known_hosts + + - name: Деплой + run: | + ssh -i ~/.ssh/deploy_key root@188.225.47.78 bash -s <<'SCRIPT' + set -e + IMAGE="${{ env.REGISTRY_IMAGE }}:latest" + CONTAINER="docs" + + echo '${{ secrets.CR_TOKEN }}' | docker login ${{ env.DOCKER_REGISTRY }} -u '${{ secrets.CR_USER }}' --password-stdin + + OLD_IMAGE_ID=$(docker images -q "$IMAGE" 2>/dev/null || true) + docker pull "$IMAGE" + + docker stop "$CONTAINER" 2>/dev/null || true + docker rm "$CONTAINER" 2>/dev/null || true + docker run -d --name "$CONTAINER" --network web --restart unless-stopped "$IMAGE" + + NEW_IMAGE_ID=$(docker images -q "$IMAGE") + if [ -n "$OLD_IMAGE_ID" ] && [ "$OLD_IMAGE_ID" != "$NEW_IMAGE_ID" ]; then + docker rmi "$OLD_IMAGE_ID" 2>/dev/null || true + fi + + docker image prune -af --filter "label=org.opencontainers.image.title=$CONTAINER" + docker image prune -f + docker builder prune -f 2>/dev/null || true + + docker ps --filter "name=$CONTAINER" + SCRIPT diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04c852e --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Generated docs +docs/*/content/ +public/llms.txt +public/slm-design/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..bc28524 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,49 @@ +:8082 { + root * /srv + + # Чистые URL: запросы вида `/foo.html` редиректим на `/foo`. + @legacyHtml { + path_regexp legacyHtml ^(/.+)\.html$ + not path /index.html + } + redir @legacyHtml {re.legacyHtml.1} 301 + + # LLM-артефакты остаются в своих пространствах документаций, без редиректа в корень. + @slmDesign path /slm-design /slm-design/* + header @slmDesign Link "; rel=\"llms\"" + + @nextjsStyleGuide path /nextjs-style-guide /nextjs-style-guide/* + header @nextjsStyleGuide Link "; rel=\"llms\"" + + @reactStyleGuide path /react-style-guide /react-style-guide/* + header @reactStyleGuide Link "; rel=\"llms\"" + + @figmaAdaptiveStandards path /figma-adaptive-standards /figma-adaptive-standards/* + header @figmaAdaptiveStandards Link "; rel=\"llms\"" + + @root { + not path /slm-design /slm-design/* /nextjs-style-guide /nextjs-style-guide/* /react-style-guide /react-style-guide/* /figma-adaptive-standards /figma-adaptive-standards/* + } + header @root Link "; rel=\"llms\"" + + @existingText { + path *.txt + file + } + header @existingText Content-Type "text/plain; charset=utf-8" + + @existingMarkdown { + path *.md + file + } + header @existingMarkdown Content-Type "text/markdown; charset=utf-8" + + @missingText { + path *.txt *.md + not file + } + respond @missingText 404 + + file_server + try_files {path} {path}.html {path}/index.html /index.html +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aee6059 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:24-alpine AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run docs:build:slm-design && npm run build + +FROM caddy:2-alpine + +COPY Caddyfile /etc/caddy/Caddyfile +COPY --from=build /app/dist /srv diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/canons/slm-design/architecture/index.md b/canons/slm-design/architecture/index.md new file mode 100644 index 0000000..4f45f1e --- /dev/null +++ b/canons/slm-design/architecture/index.md @@ -0,0 +1,114 @@ +--- +title: SLM Design +description: Назначение архитектуры, ключевые принципы и карта разделов документации +--- + +# SLM Design +Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. + +## Разделы спецификации + +Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: + +- [Слои](/architecture/layers) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. +- [Модули](/architecture/modules) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. +- [Сегменты](/architecture/segments) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. +- [Монорепозитории](/architecture/monorepo) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business. + +Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории. + +## Преимущества + +### Вертикальная организация домена + +Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы. + +### Dependency Injection без фреймворков + +Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий. + +### Разделение ответственности без перегрузки слоёв + +Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода. + +### Горизонтальная инкапсуляция + +Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. + +### Колокация по умолчанию + +Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. + +### Явное разделение каркаса и контента + +Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью. + +### Масштабирование через группировку + +При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции). + +### Адаптация к монорепозиториям + +SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы. + +## Происхождение + +SLM Design вырос на основе: + +- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей +- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое +- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию +- **Colocation Principle** — код живёт рядом с местом использования + +## Пример структуры проекта + +```text +src/ +├── app/ +│ +├── layouts/ +│ ├── main/ +│ └── dashboard/ +│ +├── screens/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── about/ +│ +├── widgets/ +│ ├── page-heading/ +│ ├── hero-section/ +│ └── promo-banner/ +│ +├── business/ +│ ├── auth/ +│ ├── catalog/ +│ ├── orders/ +│ └── chat/ +│ +├── infra/ +│ ├── theme/ +│ ├── i18n/ +│ ├── backend-api/ +│ └── logger/ +│ +├── ui/ +│ ├── button/ +│ ├── input/ +│ ├── modal/ +│ ├── toast/ +│ └── dropdown/ +│ +└── shared/ + ├── lib/ + ├── types/ + └── styles/ +``` + +## Принципы + +- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле. +- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. +- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API. +- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. diff --git a/canons/slm-design/architecture/layers.md b/canons/slm-design/architecture/layers.md new file mode 100644 index 0000000..92e7438 --- /dev/null +++ b/canons/slm-design/architecture/layers.md @@ -0,0 +1,254 @@ +--- +title: Слои +description: Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя +--- + +# Слои + +Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. + +## Определение + +**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.** + +## Группы слоёв + +Слои делятся на три группы: + +| Группа | Слои | Описание | +|--------|------|----------| +| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы | +| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит | +| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги | + +## Направление зависимостей + +Любой импорт между модулями — только через публичный API. + +``` +app → [ layouts | screens ] → widgets → business → infra → ui → shared +``` + +- `layouts` и `screens` — параллельные слои, не импортируют друг друга +- Модули одного слоя в группе «Композиция» изолированы друг от друга +- Модули одного слоя `infra` и `ui` могут импортировать друг друга через публичный API +- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую +- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях + + +## Слой App + +Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen. + +В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации. + +### Требования + +- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация +- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов +- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует +- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы +- Никем не импортируется + +## Слой Layouts + +Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar). + +```text +src/layouts/ +├── main/ +├── dashboard/ +└── auth/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую + +## Слой Screens + +Контент конкретной страницы: собирает её из модулей нижних слоёв. + +```text +src/screens/ +├── home/ +├── products/ +├── product-detail/ +├── about/ +└── contacts/ +``` + +Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/screens/ +├── shop/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── cart/ +├── account/ +│ ├── profile/ +│ ├── settings/ +│ └── order-history/ +└── info/ + ├── about/ + ├── contacts/ + └── faq/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business` + +## Слой Widgets + +Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts. + +Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget. + +```text +src/widgets/ +├── page-heading/ +├── hero-section/ +├── onboarding-checklist/ +├── promo-banner/ +└── error-boundary/ +``` + +### Требования + +- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/` +- Используется в нескольких screens или layouts + +## Слой Business + +Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами. + +Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`. + +Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки. + +```text +src/business/ +├── auth/ +├── catalog/ +├── orders/ +├── checkout/ +└── chat/ +``` + +Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/business/ +├── commerce/ +│ ├── catalog/ +│ ├── cart/ +│ ├── orders/ +│ └── checkout/ +└── communication/ + ├── chat/ + └── notifications/ +``` + +### Требования + +- Один модуль = один бизнес-домен +- Циклические зависимости между доменами запрещены +- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты +- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую +- Доменные типы (`User`, `Product`) живут здесь, не в `shared/` + +## Слой infra + +Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль. + +Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. + +Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +```text +src/infra/ +├── theme/ +├── i18n/ +├── backend-api/ +├── maps-api/ +├── logger/ +├── feature-flags/ +└── realtime/ +``` + +### Требования + +- Один модуль = один техсервис +- Импортирует `infra/`, `ui/`, `shared/` + +## Слой UI + +UI-кит без бизнес-логики: button, carousel, toast, modal. + +Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`. + +Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. + +```text +src/ui/ +├── button/ +├── input/ +├── icon/ +├── carousel/ +├── modal/ +├── toast/ +├── dropdown/ +├── tabs/ +└── tooltip/ +``` + +Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах. + +```text +src/ui/ +├── primitives/ +│ ├── button/ +│ ├── input/ +│ ├── icon/ +│ └── badge/ +└── composites/ + ├── carousel/ + ├── modal/ + ├── dropdown/ + ├── tabs/ + └── tooltip/ +``` + +### Требования + +- Не содержит бизнес-логику +- Импортирует только `ui/` и `shared/` + +## Слой Shared + +Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене. + +Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует. + +Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь. + +```text +src/shared/ +├── lib/ +├── types/ +├── styles/ +└── sprites/ +``` + +### Требования + +- Не имеет runtime-состояния diff --git a/canons/slm-design/architecture/modules.md b/canons/slm-design/architecture/modules.md new file mode 100644 index 0000000..0447c95 --- /dev/null +++ b/canons/slm-design/architecture/modules.md @@ -0,0 +1,215 @@ +--- +title: Модули +description: Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента +--- + +# Модули + +Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом. + +## Определение + +**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.** + +Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно. + +Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность. + +Главная граница модуля — не папка, а ответственность. + +## Компонент + +**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.** + +Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля. + +> Компонент отображает. Модуль организует. + +Компонент не может: + +- Импортировать код проекта за пределами родительского модуля. +- Владеть архитектурными зависимостями. +- Содержать любые компоненты. +- Содержать любые модули. +- Делать внешние запросы. +- Самостоятельно получать данные. +- Выбирать источник данных. +- Композировать данные. +- Вызывать сценарные хуки. +- Оркестрировать сценарий. +- Композировать модули. +- Решать, как устроен процесс. +- Содержать бизнес-логику. +- Содержать сценарную логику. + +Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль. + +```text +auth/ +├── ui/ +│ └── logout-button/ +│ ├── logout-button.tsx +│ ├── styles/ +│ │ └── logout-button.module.css +│ ├── types/ +│ │ └── logout-button-props.type.ts +│ └── index.ts +└── index.ts +``` + +## Что считается модулем + +Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу. + +Примеры модулей: + +- `screens/home/` — модуль страницы. +- `widgets/page-heading/` — модуль виджета. +- `business/auth/` — модуль бизнес-домена. +- `infra/theme/` — модуль инфраструктурного сервиса. +- `ui/button/` — модуль UI-kit сущности. +- `screens/home/parts/hero-section/` — вложенный модуль страницы. + +Не считаются модулями: + +- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты. +- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`. +- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента. + +## Типы модулей + +Тип модуля определяет обязательный корневой файл и стартовую структуру. + +### UI-модуль + +Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне: + +```text +header/ +├── header.tsx +└── index.ts +``` + +`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу. + +### Бизнес-модуль + +Бизнес-модуль — модуль, который строится вокруг публичного runtime API. + +Бизнес-модуль обязан иметь фабрику в корне: + +```text +auth/ +├── auth.factory.ts +├── index.ts +└── types/ +``` + +Фабрика возвращает публичный runtime API модуля. + +### Инфраструктурный модуль + +Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции. + +Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса. + +```text +theme/ +├── index.ts +├── config/ +├── hooks/ +├── styles/ +└── ui/ +``` + +```text +backend-api/ +├── backend-api.client.ts +├── config/ +├── types/ +└── index.ts +``` + +## Структура + +Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности. + +```text +{module-name}/ +├── {module-name}.factory.ts # фабрика (для business-модулей) +├── {module-name}.tsx # корневой файл модуля (опционален) +├── ui/ # компоненты модуля +├── parts/ # вложенные модули +├── hooks/ # хуки +├── stores/ # сторы состояния +├── services/ # внешние источники данных +├── mappers/ # трансформация данных между форматами +├── types/ # типы +├── styles/ # стили +├── lib/ # утилиты модуля +├── config/ # константы и конфигурация +└── index.ts # публичный API +``` + +Подробное описание сегментов — в разделе [Сегменты](/architecture/segments). + +## Публичный API + +Внешний код импортирует модуль только через публичный API. + +```ts +// Хорошо +import { customerFactory } from '@/business/customer' +import type { Customer } from '@/business/customer' +``` + +```ts +// Плохо +import { validateToken } from '@/business/auth/lib/tokens' +``` + +`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи. + +Внутренние сегменты модуля остаются деталями реализации. + +Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика. + +```ts +// business/customer/index.ts +export { customerFactory } from './customer.factory' + +export type { Customer } from './types/customer.type' +export type { CustomerApi } from './types/customer-api.type' +export type { CustomerDeps } from './types/customer-deps.type' +export type { CustomerFactory } from './types/customer-factory.type' +``` + +## Фабрика + +Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля. + +Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика. + +Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью. + +Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». + +### Примеры + +Пример реализации фабрики в React см. в [Создание фабрики](/examples/react/factory). + +Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](/examples/react/factory-composition). + +Пример композиции фабрик через React Provider см. в [Композиция через Provider](/examples/react/composition-provider). + +## Жизненный цикл + +Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. + +- Нужен на одной странице → `screens/{name}/parts/` +- Появился в 2+ местах → поднимается по природе: + - абстрактный UI → `ui/` + - блок с данными/логикой → `widgets/` + - представление бизнес-домена → `business/{area}/parts/` + +Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/canons/slm-design/architecture/monorepo.md b/canons/slm-design/architecture/monorepo.md new file mode 100644 index 0000000..0078180 --- /dev/null +++ b/canons/slm-design/architecture/monorepo.md @@ -0,0 +1,235 @@ +--- +title: Монорепозитории +description: Правила применения SLM Design для frontend-проектов, находящихся в монорепозитории +--- + +# Монорепозитории + +Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов. + +## Определение + +**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.** + +## Базовая структура + +Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`. + +```text +repo/ +├── apps/ +│ ├── web/ +│ │ └── src/ +│ │ ├── app/ +│ │ ├── layouts/ +│ │ ├── screens/ +│ │ ├── widgets/ +│ │ ├── business/ +│ │ ├── infra/ +│ │ ├── ui/ +│ │ └── shared/ +│ └── admin/ +│ └── src/ +│ └── ... +└── packages/ + ├── ui/ + │ ├── button/ # самостоятельный пакет UI-модуля + │ ├── input/ # самостоятельный пакет UI-модуля + │ └── modal/ # самостоятельный пакет UI-модуля + ├── infra/ + │ ├── theme/ # самостоятельный пакет infra-модуля + │ ├── backend-api/ # самостоятельный пакет infra-модуля + │ └── logger/ # самостоятельный пакет infra-модуля + └── shared/ # единый shared-пакет + ├── package.json + └── src/ + ├── lib/ # переиспользуемые утилиты + ├── helpers/ # переиспользуемые helpers + └── index.ts +``` + +`apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои. + +## Группировка frontend-пакетов + +Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`. + +```text +packages/ui/* # пакеты UI-модулей +packages/infra/* # пакеты infra-модулей +packages/shared # единый shared-пакет +``` + +Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM. + +## Пакет и модуль + +Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации. + +В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению. + +```text +packages/ui/button/ +packages/ui/modal/ +packages/infra/theme/ +packages/infra/backend-api/ +packages/shared/ +``` + +## Что остаётся в приложении + +Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения. + +```text +apps/web/src/app/ +apps/web/src/layouts/ +apps/web/src/screens/ +apps/web/src/widgets/ +apps/web/src/business/ +``` + +`app`, `layouts` и `screens` привязаны к роутингу, каркасу и страницам конкретного приложения. `widgets` не выносятся в пакеты, потому что это слой композиции интерфейса приложения. + +`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой. + +## Что можно выносить + +В пакеты выносится только код из `ui`, `infra` и `shared`, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория. + +| Группа | Что выносить | Пример | +|--------|--------------|--------| +| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` | +| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` | +| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` | + +Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения. + +## UI-пакеты + +В `packages/ui/*` размещаются переиспользуемые UI-модули. + +```text +packages/ui/button/ +├── package.json +└── src/ + ├── button.tsx + ├── styles/ + ├── types/ + └── index.ts +``` + +UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц. + +## Infra-пакеты + +В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули. + +```text +packages/infra/backend-api/ +├── package.json +└── src/ + ├── clients/ + ├── config/ + ├── types/ + └── index.ts +``` + +Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок. + +## Shared-пакет + +`packages/shared` является единым пакетом. + +```text +packages/shared/ +├── package.json +└── src/ + ├── lib/ + ├── helpers/ + └── index.ts +``` + +В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте. + +Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся. + +## Имена пакетов и импорты + +Путь импорта задаётся `name` в `package.json`, а не расположением директории. + +```json +{ + "name": "@repo/theme" +} +``` + +```text +packages/infra/theme/package.json +``` + +```ts +import { ThemeProvider } from '@repo/theme' +``` + +Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены. + +```ts +// Хорошо +import { Button } from '@repo/button' + +// Плохо +import { Button } from '@repo/button/src/button' +``` + +## Зависимости + +На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений. + +```text +apps → packages +packages -/→ apps +``` + +Внутри приложения продолжает действовать обычное направление зависимостей SLM. + +```text +app → [ layouts | screens ] → widgets → business → infra → ui → shared +``` + +Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях. + +## Когда не выносить + +Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API. + +Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям. + +```text +# Плохо +apps/web/src/screens/home/parts/promo-section/ +packages/ui/promo-section/ +``` + +Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным `parts/`-модулем. + +## Конфигурационные пакеты + +Конфигурационные пакеты не относятся к SLM-архитектуре. + +Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`. + +## Правила + +- SLM применяется внутри каждого `apps/{app}/src`. +- Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`. +- Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями. +- В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. +- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. +- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers. +- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях. +- `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты. +- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`. +- Пакеты не импортируют приложения. +- Межпакетные импорты идут только через публичный API. +- Deep imports внутрь пакетов запрещены. +- Локальная колокация важнее преждевременного выноса в `packages/*`. diff --git a/canons/slm-design/architecture/segments.md b/canons/slm-design/architecture/segments.md new file mode 100644 index 0000000..86cdea2 --- /dev/null +++ b/canons/slm-design/architecture/segments.md @@ -0,0 +1,181 @@ +--- +title: Сегменты +description: Сегменты внутри модуля (ui/, model/, lib/ и др.), назначение и правила размещения файлов +--- + +# Сегменты + +Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. + +## Определение + +**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** + +## Обзор + +| Сегмент | Содержимое | +|---------|------------| +| `ui/` | Презентационные компоненты родительского модуля | +| `parts/` | Вложенные модули со своими сегментами | +| `hooks/` | React-хуки | +| `stores/` | Сторы состояния | +| `services/` | Работа с внешними источниками данных | +| `mappers/` | Трансформация данных между форматами | +| `types/` | TypeScript-типы и интерфейсы | +| `styles/` | Стили | +| `lib/` | Утилиты и хелперы модуля | +| `config/` | Константы и конфигурация | + +## Сегмент ui/ + +Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля. + +Компонент в `ui/`: + +- Находится в собственной папке. +- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`. +- Не содержит любые компоненты. +- Не содержит любые модули. +- Не импортирует код проекта за пределами родительского модуля. +- Не делает внешние запросы. +- Не вызывает сценарные хуки. +- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. +- Не содержит бизнес-логику или сценарную логику. + +Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/architecture/modules#компонент). + +Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. + +```text +user/ +├── ui/ +│ ├── user-avatar/ +│ │ ├── user-avatar.tsx +│ │ ├── styles/ +│ │ │ └── user-avatar.module.css +│ │ ├── types/ +│ │ │ └── user-avatar-props.type.ts +│ │ └── index.ts +│ └── user-status/ +│ ├── user-status.tsx +│ └── index.ts +├── types/ +├── hooks/ +├── user.tsx +└── index.ts +``` + +Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`. + +## Сегмент parts/ + +Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются. + +```text +home/ +├── parts/ +│ ├── hero-section/ +│ │ ├── hero-section.tsx +│ │ ├── styles/ +│ │ ├── parts/ +│ │ │ └── top-banner/ +│ │ │ ├── top-banner.tsx +│ │ │ └── index.ts +│ │ └── index.ts +│ └── features-section/ +│ ├── features-section.tsx +│ ├── hooks/ +│ └── index.ts +├── home.screen.tsx +└── index.ts +``` + +Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности. + +Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке. + +Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше. + +## Сегмент hooks/ + +React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты. + +```text +hooks/ +├── use-auth.hook.ts +├── use-session.hook.ts +└── use-permissions.hook.ts +``` + +## Сегмент stores/ + +Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.). + +```text +stores/ +├── auth.store.ts +└── session.store.ts +``` + +## Сегмент services/ + +Работа с внешними источниками данных: API-вызовы, запросы, подписки. + +```text +services/ +├── auth.service.ts +└── token.service.ts +``` + +## Сегмент mappers/ + +Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel. + +```text +mappers/ +├── map-user.ts +├── map-product.ts +└── map-order-to-dto.ts +``` + +## Сегмент types/ + +TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов. + +```text +types/ +├── user.type.ts +└── session.type.ts +``` + +## Сегмент styles/ + +Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.). + +```text +styles/ +├── auth.module.css +└── login-form.module.css +``` + +## Сегмент lib/ + +Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов. + +```text +lib/ +├── validate-email.ts +└── format-phone.ts +``` + +Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`. + +## Сегмент config/ + +Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения. + +```text +config/ +├── routes.ts +└── constants.ts +``` diff --git a/canons/slm-design/examples/react/composition-provider.md b/canons/slm-design/examples/react/composition-provider.md new file mode 100644 index 0000000..d66ee4f --- /dev/null +++ b/canons/slm-design/examples/react/composition-provider.md @@ -0,0 +1,249 @@ +--- +title: Композиция через Provider +description: Пример композиции бизнес-фабрик screen-модуля через React Provider +--- + +# Композиция через Provider + +Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя. + +## Идея + +Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга. + +## Принципы + +1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах. +2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей. +3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`. +4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen. +5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает. +6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики. + +## Структура модуля + +```text +screens/main/ +├── main.screen.tsx +├── providers/ +│ └── main-composition.provider.tsx +├── hooks/ +│ └── use-main-composition.hook.ts +├── types/ +│ └── main-composition.type.ts +├── parts/ +│ └── featured-products/ +│ ├── featured-products.tsx +│ └── index.ts +└── index.ts +``` + +Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются. + +## Распределение по сегментам + +| Файл | Сегмент | Назначение | +|------|---------|------------| +| `main-composition.type.ts` | `types/` | TypeScript-тип композиции | +| `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент | +| `use-main-composition.hook.ts` | `hooks/` | React-хук доступа | +| `main.screen.tsx` | корень | Корневой компонент screen-модуля | +| `featured-products/` | `parts/` | Вложенный модуль со своим публичным API | + +## Тип композиции + +Файл: `screens/main/types/main-composition.type.ts`. + +Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen. + +```ts +import type { CatalogApi } from '@/business/catalog' +import type { CartApi } from '@/business/cart' + +export type MainComposition = { + catalog: CatalogApi + cart: CartApi +} +``` + +## Context и Provider + +Файл: `screens/main/providers/main-composition.provider.tsx`. + +Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве. + +Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой. + +```tsx +import { createContext, type ReactNode } from 'react' +import type { MainComposition } from '../types/main-composition.type' + +export const MainCompositionContext = createContext(null) + +type Props = { + value: MainComposition + children: ReactNode +} + +export const MainCompositionProvider = ({ value, children }: Props) => ( + + {children} + +) +``` + +## Хук доступа + +Файл: `screens/main/hooks/use-main-composition.hook.ts`. + +Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`. + +Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева. + +```ts +import { useContext } from 'react' +import { MainCompositionContext } from '../providers/main-composition.provider' + +export const useMainComposition = () => { + const ctx = useContext(MainCompositionContext) + if (!ctx) { + throw new Error('useMainComposition must be used within MainCompositionProvider') + } + return ctx +} +``` + +## Сборка графа в роутере + +Файл: `app/router.tsx`. + +Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики. + +Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента. + +```tsx +import { MainScreen, MainCompositionProvider } from '@/screens/main' +import { catalogFactory } from '@/business/catalog' +import { cartFactory } from '@/business/cart' +import { authFactory } from '@/business/auth' + +const auth = authFactory() +const catalog = catalogFactory() +const cart = cartFactory({ auth }) + +const MainRoute = () => ( + + + +) +``` + +## Корневой компонент screen + +Файл: `screens/main/main.screen.tsx`. + +Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку. + +```tsx +import { useMainComposition } from './hooks/use-main-composition.hook' +import { FeaturedProducts } from './parts/featured-products' + +export const MainScreen = () => { + const { catalog } = useMainComposition() + const { useCategories, CategoryList } = catalog + const categories = useCategories() + + return ( +
+ + +
+ ) +} +``` + +## Вложенный part + +Файл: `screens/main/parts/featured-products/featured-products.tsx`. + +Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props. + +Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую. + +```tsx +import { useMainComposition } from '../../hooks/use-main-composition.hook' + +export const FeaturedProducts = () => { + const { catalog, cart } = useMainComposition() + const { useFeatured, ProductCard } = catalog + const { addItem } = cart + const products = useFeatured() + + return ( +
+ {products.map((product) => ( + addItem(product.id)} + /> + ))} +
+ ) +} +``` + +Файл: `screens/main/parts/featured-products/index.ts`. + +```ts +export { FeaturedProducts } from './featured-products' +``` + +## Публичный API screen-модуля + +Файл: `screens/main/index.ts`. + +Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации. + +```ts +export { MainScreen } from './main.screen' +export { MainCompositionProvider } from './providers/main-composition.provider' +``` + +## Почему тип композиции не экспортируется + +Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen. + +Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen. + +```ts +import type { MainComposition } from '@/screens/main' +``` + +Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля. + +## Почему хук не экспортируется + +Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`. + +## Почему Provider экспортируется + +Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева. + +## Стабильность value + +Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`. + +Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию. + +## Расширение на другие screen-модули + +Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов. + +```text +screens/checkout/providers/checkout-composition.provider.tsx +screens/checkout/hooks/use-checkout-composition.hook.ts +screens/checkout/types/checkout-composition.type.ts +``` + +Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context. diff --git a/canons/slm-design/examples/react/factory-composition.md b/canons/slm-design/examples/react/factory-composition.md new file mode 100644 index 0000000..d1a5997 --- /dev/null +++ b/canons/slm-design/examples/react/factory-composition.md @@ -0,0 +1,52 @@ +--- +title: Композиция фабрик +description: Пример композиции business-фабрик на уровне screen-модуля в React-проекте +--- + +# Композиция фабрик + +Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов. + +## Идея + +Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик. + +## Структура screen-модуля + +```text +screens/home/ +├── home.screen.tsx +└── index.ts +``` + +## Сборка фабрик + +Файл: `screens/home/home.screen.tsx`. + +```tsx +import { customerFactory } from '@/business/customer' +import { orderFactory } from '@/business/order' + +const customer = customerFactory() +const order = orderFactory({ customer }) + +const { useOrder, OrderCard } = order + +export const HomeScreen = () => { + const currentOrder = useOrder() + + return +} +``` + +`customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи. + +## Публичный API screen-модуля + +Файл: `screens/home/index.ts`. + +```ts +export { HomeScreen } from './home.screen' +``` + +Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля. diff --git a/canons/slm-design/examples/react/factory.md b/canons/slm-design/examples/react/factory.md new file mode 100644 index 0000000..94622a6 --- /dev/null +++ b/canons/slm-design/examples/react/factory.md @@ -0,0 +1,114 @@ +--- +title: Создание фабрики +description: Пример создания фабрики business-модуля в React-проекте +--- + +# Создание фабрики + +Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API. + +## Структура business-модуля + +Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`. + +```text +business/customer/ +├── customer.factory.ts +├── hooks/ +├── types/ +│ ├── customer.type.ts +│ ├── customer-api.type.ts +│ └── customer-factory.type.ts +├── ui/ +└── index.ts +``` + +## Тип публичного API + +Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы. + +```ts +// business/customer/types/customer-api.type.ts +import type { ReactNode } from 'react' +import type { Customer } from './customer.type' + +export type CustomerCardProps = { + customer: Customer +} + +export type CustomerApi = { + useCustomer: () => Customer | null + CustomerCard: (props: CustomerCardProps) => ReactNode +} +``` + +```ts +// business/customer/types/customer-factory.type.ts +import type { CustomerApi } from './customer-api.type' + +export type CustomerFactory = () => CustomerApi +``` + +## Фабрика без зависимостей + +Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов. + +```ts +// business/customer/customer.factory.ts +import { useCustomer } from './hooks/use-customer.hook' +import { CustomerCard } from './ui/customer-card' +import type { CustomerFactory } from './types/customer-factory.type' + +export const customerFactory: CustomerFactory = () => { + return { + useCustomer, + CustomerCard, + } +} +``` + +```ts +// business/customer/index.ts +export { customerFactory } from './customer.factory' + +export type { Customer } from './types/customer.type' +export type { CustomerApi } from './types/customer-api.type' +export type { CustomerFactory } from './types/customer-factory.type' +``` + +## Фабрика с зависимостями + +Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API. + +```ts +// business/order/types/order-deps.type.ts +import type { CustomerApi } from '@/business/customer' + +export type OrderDeps = { + customer: Pick +} +``` + +```ts +// business/order/types/order-factory.type.ts +import type { OrderApi } from './order-api.type' +import type { OrderDeps } from './order-deps.type' + +export type OrderFactory = (deps: OrderDeps) => OrderApi +``` + +```ts +// business/order/order.factory.ts +import { createUseOrder } from './hooks/use-order.hook' +import { OrderCard } from './ui/order-card' +import type { OrderFactory } from './types/order-factory.type' + +export const orderFactory: OrderFactory = (deps) => { + const useOrder = createUseOrder(deps) + + return { + useOrder, + OrderCard, + } +} +``` diff --git a/canons/style-guide/DEVELOP.md b/canons/style-guide/DEVELOP.md new file mode 100644 index 0000000..523abfd --- /dev/null +++ b/canons/style-guide/DEVELOP.md @@ -0,0 +1,103 @@ +--- +title: Гид для агента +description: Что AI-агент обязан прочитать перед началом работы, а что — по задаче. +--- + +# Обязательное чтение перед началом работы + +Этот документ определяет **строгий порядок действий агента перед выполнением любых задач**. + +## Общее правило + +Перед началом работы над **любой задачей** агент **обязан ознакомиться с базовой документацией проекта**. + +Нарушение этого порядка считается ошибкой. + +--- + +## Порядок обязательного чтения + +Агент должен читать документацию **строго в следующем порядке**: + +### 1. Архитектура (КРИТИЧЕСКИ ВАЖНО) + +* [Архитектура: Обзор](./basics/architecture/index.md) +* [Архитектура: Слои](./basics/architecture/layers.md) +* [Архитектура: Модули](./basics/architecture/modules.md) +* [Архитектура: Сегменты](./basics/architecture/segments.md) + +**Архитектура — это самое важное в проекте.** + +Агент обязан: + +* строго понимать архитектурный подход (SLM) +* соблюдать архитектуру **на 100% без отклонений** +* не предлагать решений, нарушающих архитектурные принципы +* не упрощать архитектуру даже ради скорости выполнения задачи + +Любое нарушение архитектуры недопустимо. + +--- + +### 2. Базовые правила + +После архитектуры необходимо изучить: + +* [Технологии и библиотеки](./basics/tech-stack.md) +* [Именование](./basics/naming.md) +* [Стиль кода](./basics/code-style.md) +* [Документирование](./basics/documentation.md) +* [Типизация](./basics/typing.md) + +Агент обязан применять эти правила во всех решениях. + +--- + +## Использование карты документации + +Для поиска дополнительных сведений агент должен использовать: + +* [MAP.md](./MAP.md) + +MAP.md содержит ссылки на все прикладные и вспомогательные разделы. + +Агент может: + +* переходить к нужным разделам через MAP.md +* уточнять детали реализации +* искать примеры и частные случаи + +--- + +## Запрещено + +Агенту запрещено: + +* начинать выполнение задачи без изучения архитектуры +* игнорировать базовые правила +* принимать решения, противоречащие архитектуре +* придумывать собственные подходы, если они не описаны в документации + +--- + +## Ожидаемое поведение агента + +Перед выполнением задачи агент должен: + +1. Изучить архитектуру +2. Изучить базовые правила +3. При необходимости открыть MAP.md и найти релевантные разделы +4. Только после этого приступать к решению задачи + +--- + +## Приоритеты + +При принятии решений агент должен руководствоваться следующим приоритетом: + +1. **Архитектура** +2. Базовые правила +3. Документация из MAP.md +4. Задача пользователя + +Если задача противоречит архитектуре — задача должна быть переосмыслена, а не выполнена напрямую. diff --git a/canons/style-guide/MAP.md b/canons/style-guide/MAP.md new file mode 100644 index 0000000..eeafb89 --- /dev/null +++ b/canons/style-guide/MAP.md @@ -0,0 +1,59 @@ +# Карта документации + +Список всех разделов архива с относительными ссылками. Точка входа +— `DEVELOP.md` рядом с этим файлом. + +## Подсказки + +- [Подсказки](./workflow.md) — Короткие ответы на типовые вопросы и решения для спорных ситуаций. + +## Базовые правила + +- [Технологии и библиотеки](./basics/tech-stack.md) — Какие библиотеки и инструменты используются в проекте. +- [Именование](./basics/naming.md) — Как называть переменные, файлы и прочие сущности в коде. +- [Архитектура: Обзор](./basics/architecture/index.md) — Архитектурный подход проекта: что такое SLM и как он устроен. +- [Архитектура: Слои](./basics/architecture/layers.md) — Из каких слоёв состоит SLM-архитектура и как они связаны. +- [Архитектура: Модули](./basics/architecture/modules.md) — Что такое модуль в SLM-архитектуре и как он устроен. +- [Архитектура: Сегменты](./basics/architecture/segments.md) — Что такое сегмент модуля в SLM-архитектуре и какие они бывают. +- [Стиль кода](./basics/code-style.md) — Как оформляется код в проекте. +- [Документирование](./basics/documentation.md) — Что и как документировать в коде. +- [Типизация](./basics/typing.md) — Как типизируется код в проекте. + +## Прикладные разделы + +- [Создание проекта: Из шаблона](./applied/creating-project/from-template.md) — Создание нового проекта на основе готового шаблона. +- [Создание проекта: По гайду вручную](./applied/creating-project/manual.md) — Поэтапное создание нового проекта без использования шаблона. +- [Создание проекта: Чистый Next.js](./applied/creating-project/nextjs.md) — Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку. +- [Структура проекта](./applied/project-structure.md) — Из чего состоит проект и где что лежит. +- [Страницы](./applied/page-level.md) — Как работать со страницами и другими файлами роутинга Next.js App Router. +- [Компонент](./applied/component.md) — Как создавать React-компоненты внутри SLM-модулей. +- [Модуль](./applied/module.md) — Как создавать и организовывать SLM-модули в проекте. +- [REST-клиент](./applied/rest-client/index.md) — Настройка REST-клиента сервиса для работы с внешним API. +- [REST-клиент: Настройка REST-клиента](./applied/rest-client/setup/index.md) — Из чего состоит REST-клиент и что подготовить перед использованием API. +- [REST-клиент: Автогенерация REST-клиента](./applied/rest-client/setup/auto.md) — Генерация REST-клиента из OpenAPI-спецификации. +- [REST-клиент: Ручное создание REST-клиента](./applied/rest-client/setup/manual.md) — Создание REST-клиента вручную, когда OpenAPI нет или он неполный. +- [REST-клиент: GET-хуки REST-клиента](./applied/rest-client/setup/hooks.md) — Прозрачные SWR-обёртки над GET-методами REST-клиента. +- [REST-клиент: Использование REST-клиента](./applied/rest-client/usage.md) — Как вызывать готовый REST-клиент в серверном коде и submit-функциях. +- [Получение данных](./applied/data-fetch/index.md) — Как получать данные с учётом рендера страницы. +- [Получение данных: Серверный await](./applied/data-fetch/server-await.md) — Получение REST-данных на сервере до первого HTML. +- [Получение данных: Параллельные серверные запросы](./applied/data-fetch/parallel-server-requests.md) — Как запускать независимые REST-запросы на сервере без waterfall. +- [Получение данных: Передача промиса ниже](./applied/data-fetch/pass-promise-down.md) — Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте. +- [Получение данных: Начальные данные для клиентских хуков](./applied/data-fetch/client-hooks-initial-data.md) — Как дать клиентским GET-хукам начальные REST-данные. +- [Получение данных: Клиентский GET-хук](./applied/data-fetch/client-get-hook.md) — Получение REST-данных в Client Components через готовые GET-хуки REST-клиента. +- [Получение данных: Business-композиция](./applied/data-fetch/business-composition.md) — Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле. +- [Стили: Настройка](./applied/styles/styles-setup.md) — Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили. +- [Стили: Использование](./applied/styles/styles-usage.md) — Как пишутся стили в проекте. +- [SVG-спрайты](./applied/svg-sprites/svg-sprites-intro.md) — Что такое SVG-спрайты и какие проблемы они решают. +- [SVG-спрайты: Настройка](./applied/svg-sprites/svg-sprites-setup.md) — Подключение SVG-спрайтов в новом проекте. +- [SVG-спрайты: Использование](./applied/svg-sprites/svg-sprites-usage.md) — Как добавлять и использовать SVG-иконки в коде. +- [Изображения](./applied/images.md) — Как подключать изображения через Next.js Image в проекте. +- [Шрифты](./applied/fonts.md) — Как подключать шрифты через Next.js Font в проекте. +- [Алиасы импортов](./applied/aliases.md) — Какие алиасы импортов есть в проекте и как ими пользоваться. +- [Шаблоны генерации](./applied/templates/templates-intro.md) — Что такое шаблоны кодогенерации и какие проблемы они решают. +- [Шаблоны генерации: Настройка](./applied/templates/templates-setup.md) — Первичная установка шаблонов кодогенерации в проект. +- [Шаблоны генерации: Создание шаблонов](./applied/templates/templates-create.md) — Структура шаблонов, синтаксис переменных и примеры. +- [Шаблоны генерации: Использование](./applied/templates/templates-usage.md) — Генерация файлов из шаблонов через VS Code плагин и CLI. +- [Biome](./applied/biome.md) — Установка и настройка линтера-форматтера в новом проекте. +- [PostCSS](./applied/postcss.md) — Установка и настройка CSS-процессора в новом проекте. +- [VS Code](./applied/vscode.md) — Единые настройки редактора и расширений для команды. +- [Локализация](./applied/localization.md) — Как организовать локализацию как infra-модуль. diff --git a/canons/style-guide/applied/aliases.md b/canons/style-guide/applied/aliases.md new file mode 100644 index 0000000..8fce894 --- /dev/null +++ b/canons/style-guide/applied/aliases.md @@ -0,0 +1,77 @@ +--- +title: Алиасы импортов +description: Какие алиасы импортов есть в проекте и как ими пользоваться. +keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infra, ui, shared] +--- + +# Алиасы импортов + +Какие алиасы импортов есть в проекте и как ими пользоваться. + +## Конфиг + +`tsconfig.json` в корне проекта: + +```json +{ + "compilerOptions": { + "paths": { + "app/*": ["./src/app/*"], + "layouts/*": ["./src/layouts/*"], + "screens/*": ["./src/screens/*"], + "widgets/*": ["./src/widgets/*"], + "business/*": ["./src/business/*"], + "infra/*": ["./src/infra/*"], + "ui/*": ["./src/ui/*"], + "shared/*": ["./src/shared/*"] + } + } +} +``` + +Восемь алиасов — ровно по числу слоёв. Других алиасов в проекте нет. + +## Правила + +- **Каждый импорт между модулями — через алиас слоя.** Относительные пути (`../../`) запрещены за пределами своего модуля. +- **Внутри одного модуля** допустимы относительные импорты (`./model`, `./ui/button`) — это часть инкапсуляции модуля. +- **Префикс `@/` не используется.** Имя слоя — само по себе адрес. +- **Направление импортов** определяется архитектурой, не алиасами. Алиас разрешает импорт технически, но не отменяет правила слоёв (→ [Слои](/docs/basics/architecture/layers)). + +**Хорошо** + +```ts +import { Button } from 'ui/button' +import { useUser } from 'business/user' +import { formatDate } from 'shared/utils/date' +``` + +**Плохо** + +```ts +// Относительный путь между модулями +import { Button } from '../../../ui/button' + +// Префикс @/, которого нет в paths +import { Button } from '@/ui/button' + +// Алиас на src — не предусмотрен +import { Button } from 'src/ui/button' +``` + +## Внутри модуля + +Внутри своего модуля — относительные пути: + +```ts +// src/ui/button/button.tsx +import styles from './button.module.css' +import { Icon } from './icon' +``` + +Не использовать алиас на самого себя: + +```ts +// Плохо — алиас вместо относительного пути внутри модуля +import { Icon } from 'ui/button/icon' +``` diff --git a/canons/style-guide/applied/biome.md b/canons/style-guide/applied/biome.md new file mode 100644 index 0000000..cc8034a --- /dev/null +++ b/canons/style-guide/applied/biome.md @@ -0,0 +1,114 @@ +--- +title: Biome +description: Установка и настройка линтера-форматтера в новом проекте. +keywords: [biome, линтер, форматтер, lint, format, biome.json, "@biomejs/biome", замена eslint, замена prettier] +--- + +# Biome + +Установка и настройка линтера-форматтера в новом проекте. + +## Требования + +- Node.js 18+. +- Проект без установленного ESLint и Prettier (они конфликтуют с Biome). + +## Установка + +1. Установить пакет: + + ```bash + npm install --save-dev --save-exact @biomejs/biome + ``` + +2. Инициализировать конфиг: + + ```bash + npx @biomejs/biome init + ``` + + В корне появится `biome.json` с дефолтными настройками. + +3. Привести `biome.json` к стандартному виду (см. «Стандартный `biome.json`»). Делается сразу после `init`, до первого запуска `lint`/`check`. + +4. Добавить скрипты в `package.json`: + + ```json + { + "scripts": { + "lint": "biome lint .", + "format": "biome format --write .", + "check": "biome check --write ." + } + } + ``` + + | Скрипт | Что делает | + |--------|-----------| + | `lint` | Проверка правил без правок | + | `format` | Автоформатирование всех файлов | + | `check` | Lint + format + organize imports в один проход (основная команда) | + +## Стандартный `biome.json` + +Дефолтный `biome.json`, созданный `biome init`, заменяется стандартным конфигом проекта. + +Стандартный `biome.json`: + +```jsonc +{ + "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": ["**", "!node_modules", "!.next", "!dist", "!build", "!.templates", "!src/infra/**/generated"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 120 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noUnknownAtRules": "off" + }, + "correctness": { + "noUnknownMediaFeatureName": "off" + } + }, + "domains": { + "next": "recommended", + "react": "recommended" + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} +``` + +`src/infra/**/generated` исключается из Biome, потому что generated-файлы не правятся руками. При этом generated-файлы остаются в git. + +Правила `suspicious/noUnknownAtRules` и `correctness/noUnknownMediaFeatureName` отключены, потому что проектный CSS-стек использует `@custom-media` и другие конструкции, которые Biome может не распознавать. + +## Интеграция с VS Code + +Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [VS Code](/docs/applied/vscode). diff --git a/canons/style-guide/applied/component.md b/canons/style-guide/applied/component.md new file mode 100644 index 0000000..da8f0b8 --- /dev/null +++ b/canons/style-guide/applied/component.md @@ -0,0 +1,165 @@ +--- +title: Компонент +description: Как должен выглядеть сгенерированный React-компонент внутри SLM-модуля. +--- + +# Компонент + +Как должен выглядеть сгенерированный React-компонент внутри SLM-модуля. + +## Назначение + +Архитектурное определение компонента описано в разделе [Модули → Компонент](/docs/basics/architecture/modules#компонент), а структура сегмента `ui/` — в разделе [Сегменты → ui/](/docs/basics/architecture/segments#сегмент-ui). + +Эта страница не повторяет архитектурные ограничения. Она показывает, каким должен быть результат генерации компонента: структура папки, `.tsx`, типы, стили и локальный экспорт. + +::: danger Компоненты не создаются вручную +Компоненты в проекте создаются только через кодогенератор: через [VS Code](/docs/applied/templates/templates-usage#через-vs-code) или [CLI](/docs/applied/templates/templates-usage#через-cli). + +Ручное создание компонента запрещено. Это грубое нарушение правил работы в проекте для разработчика и AI-ассистента. + +Если в проекте нет шаблона `.templates/component`, сначала создайте шаблон по разделу [Создание шаблонов](/docs/applied/templates/templates-create), и только потом генерируйте компонент на его основе. +::: + +## Создание + +1. Проверьте, что в проекте есть шаблон `.templates/component`. +2. Если шаблона нет — создайте его по разделу [Создание шаблонов](/docs/applied/templates/templates-create). +3. Сгенерируйте компонент через [VS Code или CLI](/docs/applied/templates/templates-usage). + +Структура и код ниже показывают ожидаемый результат генерации. Их нельзя использовать как инструкцию для ручного создания файлов. + +## Структура + +Компонент размещается в `ui/{component-name}/` родительского модуля. + +Для каждого компонента обязательны `.tsx`, типы, стили и локальный `index.ts`. + +```text +user-card/ +└── ui/ + └── user-status/ + ├── styles/ + │ └── user-status.module.css + ├── types/ + │ └── user-status-props.type.ts + ├── user-status.tsx + └── index.ts +``` + +## Реализация + +Пример ниже показывает файлы базового компонента. + +### Типы + +Файл типов делится на три части: + +- `UserStatusParams` — собственные параметры компонента. Здесь лежат только данные, которые нужны именно этому компоненту. +- `RootAttrs` — параметры корневой обёртки: `div`, `span`, `a`, `button` или другого HTML-элемента. Если компонент сам управляет `children`, они исключаются через `Omit`. +- `UserStatusProps` — итоговые пропсы компонента. Тип объединяет собственные параметры и параметры корневой обёртки. + +Собственные параметры и их поля документируются по правилам раздела [Документирование → Типы, интерфейсы, enum](/docs/basics/documentation#типы-интерфейсы-enum). + +`user-card/ui/user-status/types/user-status-props.type.ts` + +```ts +import type { ComponentPropsWithoutRef } from 'react' + +/** + * Параметры UserStatus. + */ +export type UserStatusParams = { + /** Текст статуса пользователя. */ + label: string + /** Доступен ли пользователь сейчас. */ + isOnline: boolean +} + +/** Атрибуты корневого элемента без children. */ +type RootAttrs = Omit, 'children'> + +export type UserStatusProps = RootAttrs & UserStatusParams +``` + +### TSX + +В `.tsx` лежит только сам компонент: + +- Компонент объявляется через `const` и именованный экспорт. +- `React.FC` не используется. +- Параметры компонента типизируются через `Props`. +- Возвращаемый тип не указывается: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата. +- JSDoc-комментарий обязателен и пишется по правилам раздела [Документирование → Компоненты](/docs/basics/documentation#компоненты). +- Пропсы деструктурируются в теле компонента, а не в сигнатуре. +- Из пропсов обязательно выделяются `className` и `...rootAttrs`. +- Функция конкатенации CSS-классов импортируется и именуется `cl`. +- Корневой CSS-класс всегда называется `.root`. + +Комментарий описывает назначение и сценарии применения компонента, а не DOM-разметку или внутреннюю реализацию. + +`className` — внешний CSS-класс, который родитель может передать компоненту. `rootAttrs` — остальные атрибуты корневой обёртки: `id`, `aria-*`, `data-*`, обработчики событий и другие HTML-атрибуты. Они прокидываются на корневой DOM-элемент компонента. + +`.root` нужен, чтобы в DevTools быстро находить корневой DOM-узел компонента и одинаково подключать внешний `className` к реальному корню. + +`user-card/ui/user-status/user-status.tsx` + +```tsx +import cl from 'clsx' +import type { UserStatusProps } from './types/user-status-props.type' +import styles from './styles/user-status.module.css' + +/** + * Статус пользователя в карточке профиля. + * + * Используется для: + * - отображения текущей доступности пользователя + * - визуального выделения онлайн- и офлайн-состояний + */ +export const UserStatus = (props: UserStatusProps) => { + const { label, isOnline, className, ...rootAttrs } = props + + return ( + + {label} + + ) +} +``` + +### Стили + +`user-card/ui/user-status/styles/user-status.module.css` + +```css +.root { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--color-text-muted); +} + +.root::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} + +.online { + color: var(--color-success); +} +``` + +### Локальный экспорт + +`user-card/ui/user-status/index.ts` + +```ts +export { UserStatus } from './user-status' +export type { UserStatusProps } from './types/user-status-props.type' +``` diff --git a/canons/style-guide/applied/creating-project/from-template.md b/canons/style-guide/applied/creating-project/from-template.md new file mode 100644 index 0000000..3a760e2 --- /dev/null +++ b/canons/style-guide/applied/creating-project/from-template.md @@ -0,0 +1,51 @@ +--- +title: Создание проекта из шаблона +description: Создание нового проекта на основе готового шаблона. +keywords: [создать проект из шаблона, шаблон, template, tiged, degit, клонировать шаблон, эталонный шаблон, быстрый старт, scaffold, новый проект] +--- + +# Создание проекта из шаблона + +Создание нового проекта на основе готового шаблона. + +## Что внутри + +Шаблон — готовый скелет проекта с применёнными правилами стайлгайда: + +- **Стек:** Next.js (App Router), TypeScript, React. +- **Архитектура:** структура папок по SLM, алиасы импортов. +- **Качество кода:** Biome (линтер и форматтер), настройки VS Code. +- **Стили:** PostCSS Modules с плагинами, токены, медиа-брейкпоинты. +- **Ассеты:** генерация SVG-спрайтов. +- **Кодогенерация:** шаблоны для страниц, компонентов, хуков, сторов. + +## Установка + +1. Склонировать шаблон в родительском каталоге будущего проекта: + + ```bash + npx tiged git@gromlab.ru:templates/nextjs.git my-app + ``` + + `tiged` копирует снимок репозитория без истории git. Имя каталога (`my-app`) заменяется на нужное. + +2. Установить зависимости: + + ```bash + cd my-app + npm install + ``` + +3. Проверить сборку: + + ```bash + npm run build + ``` + + Сборка должна завершиться без ошибок. + +## Правила + +- **Шаблон — источник истины.** Не добавлять, не удалять и не переименовывать файлы шаблона «для приведения к канону»: шаблон уже канонический. Любое несоответствие — баг шаблона, а не проекта. +- **Менеджер пакетов — npm.** Отклонение (pnpm, yarn, bun) — только по явному решению с пониманием, что стайлгайд этого не предусматривает. +- **Не инициализировать git заново** автоматически. `tiged` намеренно не создаёт `.git/` — решение о репозитории принимает разработчик. diff --git a/canons/style-guide/applied/creating-project/manual.md b/canons/style-guide/applied/creating-project/manual.md new file mode 100644 index 0000000..764a050 --- /dev/null +++ b/canons/style-guide/applied/creating-project/manual.md @@ -0,0 +1,90 @@ +--- +title: Создание проекта вручную +description: Поэтапное создание нового проекта без использования шаблона. +keywords: [создать проект, новый проект, с нуля, init, initialize, scaffold, create-next-app, начать проект, поднять проект, эталонный проект, ручная установка] +--- + +# Создание проекта вручную + +Поэтапное создание нового проекта без использования шаблона. + +## Состав эталонного проекта + +| Компонент | Роль | Раздел | +|-----------|------|--------| +| Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](/docs/applied/creating-project/nextjs) | +| Алиасы | Импорты по слоям SLM | [Алиасы](/docs/applied/aliases) | +| Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](/docs/applied/biome) | +| Стили | Глобальные токены и breakpoints | [Стили](/docs/applied/styles/styles-setup) | +| PostCSS | CSS-процессор для custom-media и вложенности | [PostCSS](/docs/applied/postcss) | +| SVG-спрайты | Иконки через ``, управление цветом | [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-setup) | +| VS Code | Настройки редактора и расширения | [VS Code](/docs/applied/vscode) | +| Шаблоны генерации | `.templates/` для `@gromlab/create` | [Шаблоны генерации](/docs/applied/templates/templates-setup) | + +Убрать компонент из состава — значит согласованно отказаться от части стайлгайда. Частичные проекты возможны (только Next.js, Next.js + стили и т.п.), но не являются эталоном. + +## Канон раскладки + +В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)). + +В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`. + +## Порядок установки + +Подсистемы ставятся в фиксированном порядке — он отражает зависимости между шагами. + +### 1. Next.js + +Скелет фреймворка — обязательный первый шаг, остальное опирается на него. + +См. [Next.js](/docs/applied/creating-project/nextjs). После выполнения проверки этого раздела `npm run build` должен проходить. + +### 2. Алиасы + +Заменить дефолтный `"@/*"` в `tsconfig.json` на канонический список из восьми слой-префиксов. + +См. [Алиасы](/docs/applied/aliases). + +### 3. Biome + +Линтер и форматтер. Подключается **до** написания кода, иначе в проекте копятся несогласованные правки. + +См. [Biome](/docs/applied/biome). + +### 4. Стили (базовая инфраструктура) + +Файлы `variables.css`, `media.css`, `global.css` в `src/shared/styles/` и подключение `global.css` в `src/app/layout.tsx`. CSS-процессор на этом шаге не ставится. + +См. [Стили](/docs/applied/styles/styles-setup). + +### 5. PostCSS + +CSS-процессор поверх базовых стилей: `@custom-media`, вложенность, autoprefixer. Ставится **только после шага 4** — опирается на `src/shared/styles/media.css`. + +См. [PostCSS](/docs/applied/postcss). + +### 6. SVG-спрайты + +Пакет `@gromlab/svg-sprites`, генерация спрайт-файла и React-компонента ``. + +См. [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-setup). + +### 7. VS Code + +Расширения и настройки редактора. Опирается на установленный Biome (форматирование при сохранении) и PostCSS (ассоциация `*.css`). + +См. [VS Code](/docs/applied/vscode). + +### 8. Шаблоны генерации + +Папка `.templates/` для генератора модулей `@gromlab/create`. + +См. [Шаблоны генерации](/docs/applied/templates/templates-setup). + +## Правила + +- **Порядок шагов фиксирован.** Перестановка ломает зависимости (PostCSS требует базовых стилей, VS Code — установленного Biome). +- **Между шагами обязательна проверка** из соответствующего раздела. Не переходить дальше, пока чеклист текущего шага не пройден. +- **Слои `src/`** (`layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6). +- **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены. +- **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство. diff --git a/canons/style-guide/applied/creating-project/nextjs.md b/canons/style-guide/applied/creating-project/nextjs.md new file mode 100644 index 0000000..edc2302 --- /dev/null +++ b/canons/style-guide/applied/creating-project/nextjs.md @@ -0,0 +1,112 @@ +--- +title: Чистая установка Next.js +description: "Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку." +keywords: [next.js, create-next-app, npx, установка, инициализация, фреймворк, скаффолдинг, app router, typescript] +--- + +# Чистая установка Next.js + +Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку. + +## Требования + +- Node.js 18.18+ (рекомендуется LTS 20+). +- npm 10+. +- Рабочая папка пуста, либо для установки выбрана подпапка (`create-next-app@16+` отказывается ставиться в непустую директорию). + +## Установка + +### 1. Инициализация через `create-next-app` + +Флаги зафиксированы и не согласовываются — это канон стайлгайда: + +```bash +npx create-next-app@latest my-app \ + --typescript \ + --app \ + --src-dir \ + --import-alias "@/*" \ + --no-eslint \ + --no-tailwind \ + --use-npm +``` + +| Флаг | Значение | Почему так | +|------|----------|------------| +| `--typescript` | TS включён | Стайлгайд требует TypeScript ([Типизация](/docs/basics/typing)) | +| `--app` | App Router | Pages Router не используется | +| `--src-dir` | Код в `src/` | Архитектура SLM требует `src/` ([Структура проекта](/docs/applied/project-structure)) | +| `--import-alias "@/*"` | Placeholder | Требуется флагом; после установки `paths` полностью переписывается на слой-префиксы (см. [Алиасы](/docs/applied/aliases)) | +| `--no-eslint` | ESLint не ставится | Линтер и форматтер — Biome ([Biome](/docs/applied/biome)) | +| `--no-tailwind` | Tailwind не ставится | Стилизация — PostCSS Modules ([Стили](/docs/applied/styles/styles-usage)) | +| `--use-npm` | Пакетный менеджер — npm | Единый инструмент в проектах | + +### 2. Очистить дефолтный шаблон + +`create-next-app` генерирует демо-страницу со стилями и ассетами, а Next.js 16+ дополнительно кладёт в корень собственные `AGENTS.md` и `CLAUDE.md` — всё это удаляется. + +```bash +rm src/app/page.module.css +rm src/app/globals.css +rm public/next.svg public/vercel.svg public/file.svg public/globe.svg public/window.svg +rm -f AGENTS.md CLAUDE.md +``` + +Заменить `src/app/page.tsx` на минимальный: + +```tsx +// src/app/page.tsx +export default function HomePage() { + return

Home

+} +``` + +Очистить `src/app/layout.tsx` от импорта шрифтов и `globals.css`: + +```tsx +// src/app/layout.tsx +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'App', + description: '', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} +``` + +### 3. Создать папку `src/shared/styles/` + +Глобальные стили в SLM-архитектуре живут в слое `shared`, а не в `src/app/` ([Структура проекта](/docs/applied/project-structure)). + +```bash +mkdir -p src/shared/styles +``` + +Остальные слои (`layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`) заводятся при появлении первого модуля в них. `src/shared/styles/` — единственный подкаталог `shared/`, который заводится сразу: без него не настроить стили на следующих шагах. + +## Правила + +- **Конфликт с непустой директорией** — не удалять файлы пользователя автоматически. Ставить в подпапку или временно перенести посторонние файлы. +- **Отклонение от канонических флагов** (pnpm, Tailwind, ESLint и т.п.) — только осознанное, с пониманием, что стайлгайд этого не предусматривает. +- **Слои `src/`** не создавать авансом — появляются при первом модуле. Алиасы прописываются сразу на все восемь слоёв (см. [Алиасы](/docs/applied/aliases)). +- **`AGENTS.md` от Next.js** удаляется в шаге 2. Повторно не создаётся. +- **Biome, стили, PostCSS, SVG-спрайты, VS Code** — отдельные шаги установки, не в этом разделе. + +## Проверка установки + +- В корне проекта: `next.config.ts`, `tsconfig.json`, `package.json`. +- В `package.json`: Next.js установлен, нет `eslint`, `tailwindcss`. +- В `src/app/` присутствуют минимальные `page.tsx` и `layout.tsx`. `globals.css`, `page.module.css` отсутствуют. Каталогов `styles/`, `assets/`, `providers/`, `components/` в `src/app/` нет. +- Папка `src/shared/styles/` создана (пустая). +- В `src/` из слоёв SLM присутствуют только `app/` и `shared/` (с `styles/`). Посторонних каталогов нет. +- В `public/` удалены `next.svg`, `vercel.svg`, `file.svg`, `globe.svg`, `window.svg`. +- В корне проекта нет `AGENTS.md` и `CLAUDE.md` от Next.js. +- `npm run build` завершается успешно. +- Пакетный менеджер — npm (нет `pnpm-lock.yaml`, `yarn.lock`, `bun.lockb`). diff --git a/canons/style-guide/applied/data-fetch/business-composition.md b/canons/style-guide/applied/data-fetch/business-composition.md new file mode 100644 index 0000000..09202e8 --- /dev/null +++ b/canons/style-guide/applied/data-fetch/business-composition.md @@ -0,0 +1,121 @@ +--- +title: Business-композиция +description: Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле. +keywords: [rest, business, композиция, hooks, domain, isAuth] +--- + +# Business-композиция + +Business-композиция используется, когда простого GET-метода или прозрачного GET-хука недостаточно: нужно объединить несколько источников, преобразовать DTO или вычислить доменное состояние. + +## Когда использовать + +- Нужно объединить несколько GET-запросов. +- Нужно вычислить `isAuth`, `canEdit`, `hasAccess`, `hasPets`. +- Нужно преобразовать DTO в доменную модель. +- Нужно спрятать бизнес-сценарий за доменным API. + +Такая логика не пишется в `infra/`. REST-клиент остаётся прозрачным адаптером к API. + +## Пример поверх одного GET-хука + +```ts +// src/business/pets/hooks/use-available-pets.hook.ts +import { StatusEnum, useGetPetList } from 'infra/pet-store-api' + +/** + * Доменный список доступных питомцев. + */ +export const useAvailablePets = () => { + const query = useGetPetList({ status: StatusEnum.Available }) + + return { + ...query, + hasPets: Boolean(query.data?.length), + } +} +``` + +`useGetPetList` — infra-хук. `hasPets` — бизнес-интерпретация, поэтому она появляется в `business/pets`. + +## Пример композиции нескольких GET-хуков + +```ts +// src/business/pets/hooks/use-pets-dashboard.hook.ts +import { StatusEnum, useGetPetList } from 'infra/pet-store-api' + +/** + * Данные dashboard по питомцам. + */ +export const usePetsDashboard = () => { + const availablePets = useGetPetList({ status: StatusEnum.Available }) + const pendingPets = useGetPetList({ status: StatusEnum.Pending }) + const soldPets = useGetPetList({ status: StatusEnum.Sold }) + + return { + availablePets, + pendingPets, + soldPets, + total: + (availablePets.data?.length ?? 0) + + (pendingPets.data?.length ?? 0) + + (soldPets.data?.length ?? 0), + } +} +``` + +Композиция нескольких запросов не добавляется в `infra/pet-store-api/hooks/`, потому что это уже сценарий потребления данных. + +## Пример auth-состояния + +```ts +// src/business/auth/hooks/use-auth-state.hook.ts +import { useGetCurrentUser } from 'infra/backend-api' + +/** + * Состояние авторизации текущего пользователя. + */ +export const useAuthState = () => { + const currentUser = useGetCurrentUser() + const user = currentUser.data + + return { + ...currentUser, + user, + isAuth: Boolean(user), + } +} +``` + +`isAuth` не является частью REST-клиента. Это доменный смысл результата запроса. + +## Где размещать + +```text +src/business/ +└── pets/ + ├── hooks/ + │ └── use-available-pets.hook.ts + ├── mappers/ + │ └── map-pet-dto-to-pet.ts + ├── types/ + └── index.ts +``` + +Модуль `business/` экспортирует наружу готовый доменный API через `index.ts`. + +## Что запрещено + +```ts +// Плохо — business-смысл внутри infra-хука +export const useGetPetList = (params?: FindPetsByStatusParams | null) => { + const query = useSWR(...) + + return { + ...query, + hasPets: Boolean(query.data?.length), + } +} +``` + +REST-модуль отвечает за доступ к API. Business-модуль отвечает за смысл этих данных в продукте. diff --git a/canons/style-guide/applied/data-fetch/client-get-hook.md b/canons/style-guide/applied/data-fetch/client-get-hook.md new file mode 100644 index 0000000..cccbc73 --- /dev/null +++ b/canons/style-guide/applied/data-fetch/client-get-hook.md @@ -0,0 +1,88 @@ +--- +title: Клиентский GET-хук +description: Получение REST-данных в Client Components через готовые GET-хуки REST-клиента. +keywords: [rest, client components, swr, get-хук, client state] +--- + +# Клиентский GET-хук + +Клиентский GET-хук используется, когда данные зависят от состояния браузера: вкладки, фильтра, поиска, пагинации, модалки или действия пользователя. + +## Когда использовать + +- Запрос зависит от client state. +- Данные не обязательны для первого HTML. +- Пользователь меняет параметры запроса на клиенте. +- Нужны SWR-кеширование, дедупликация и ревалидация. + +## Пример с вкладками + +```tsx +'use client' + +import { useState } from 'react' +import { StatusEnum, useGetPetList } from 'infra/pet-store-api' + +const statuses = [StatusEnum.Available, StatusEnum.Pending, StatusEnum.Sold] + +export function PetTabs() { + const [status, setStatus] = useState(StatusEnum.Available) + const { data: pets, isLoading, error } = useGetPetList({ status }) + + return ( +
+
+ {statuses.map((item) => ( + + ))} +
+ + {isLoading &&
Загрузка...
} + {error &&
Ошибка загрузки
} + +
    + {pets?.map((pet) => ( +
  • {pet.name}
  • + ))} +
+
+ ) +} +``` + +Компонент выбирает параметр `status`, но не знает про SWR-ключ и fetcher. Запрос выполняет готовый GET-хук REST-клиента. + +## Если хука нет + +Хук добавляется в REST-модуль сервиса: + +```text +src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts +``` + +Не создавайте локальный `useSWR` в компоненте. + +## Плохо + +```tsx +// Плохо — прямой вызов клиента в useEffect +useEffect(() => { + petStoreApi.pet.findPetsByStatus({ status }).then(setPets) +}, [status]) + +// Плохо — useSWR в компоненте +const { data } = useSWR( + ['pet-store-api', `/pet/findByStatus?status=${status}`], + () => petStoreApi.pet.findPetsByStatus({ status }), +) +``` + +Такой код теряет единое место для ключей, дублирует fetcher и разносит инфраструктурные детали по UI. + +## Когда выбрать другую стратегию + +- Данные нужны до первого HTML — [Серверный await](/docs/applied/data-fetch/server-await). +- Клиентский хук должен получить начальные данные сразу — [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data). +- Нужно вычислить бизнес-состояние — [Business-композиция](/docs/applied/data-fetch/business-composition). diff --git a/canons/style-guide/applied/data-fetch/client-hooks-initial-data.md b/canons/style-guide/applied/data-fetch/client-hooks-initial-data.md new file mode 100644 index 0000000..9328c12 --- /dev/null +++ b/canons/style-guide/applied/data-fetch/client-hooks-initial-data.md @@ -0,0 +1,117 @@ +--- +title: Начальные данные для клиентских хуков +description: Как дать клиентским GET-хукам начальные REST-данные. +keywords: [rest, swr, fallback, initial data, client hooks, unstable_serialize, isr, ssr] +--- + +# Начальные данные для клиентских хуков + +Как дать клиентским GET-хукам начальные REST-данные. + +Эта стратегия используется, когда данные должны быть запущены на сервере, но потребляться на клиенте через GET-хуки REST-клиента. + +Технически это делается через `SWRConfig fallback`: сервер передаёт промис в fallback, а клиентский хук использует тот же SWR-ключ. + +## Когда использовать + +- Внутри страницы есть Client Components с GET-хуками. +- Нужно начать загрузку данных на сервере раньше. +- Клиентский компонент должен остаться обычным потребителем `useGetPetList(...)`. +- Не нужно писать отдельный prop-drilling для начальных данных. + +## Рендер страницы + +Перед этой стратегией сначала определите рендер маршрута. Серверный preload для `fallback` подчиняется тем же правилам, что и любой серверный запрос в `page.tsx` или `layout.tsx`. + +Если данные общие и могут обновляться по интервалу, сохраняйте static/ISR. Если preload зависит от cookie, headers, `searchParams`, `no-store` или персональных данных пользователя, маршрут становится dynamic/SSR. + +`SWRConfig fallback` не должен быть причиной отключать ISR на всякий случай. Он только передаёт клиентскому GET-хуку данные, которые уже были запущены на сервере. + +## Ключ хука + +```ts +// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts +export const getPetListKey = (params?: FindPetsByStatusParams | null) => { + if (!params?.status) { + return null + } + + return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const +} +``` + +Ключ экспортируется из REST-модуля, потому что он нужен и GET-хуку, и серверному `SWRConfig fallback`. + +## Пример layout + +```tsx +// src/app/(routes)/pets/layout.tsx +import type { ReactNode } from 'react' +import { SWRConfig, unstable_serialize } from 'swr' +import { + getPetListKey, + petStoreApi, + StatusEnum, +} from 'infra/pet-store-api' + +type PetsLayoutProps = { + children: ReactNode +} + +export default async function PetsLayout({ children }: PetsLayoutProps) { + const params = { status: StatusEnum.Available } + const availablePetsPromise = petStoreApi.pet.findPetsByStatus(params) + + return ( + + {children} + + ) +} +``` + +Если GET-хук использует array-key, ключ для `fallback` сериализуется через `unstable_serialize`. + +## Клиентский компонент + +```tsx +'use client' + +import { StatusEnum, useGetPetList } from 'infra/pet-store-api' + +export function PetList() { + const { data: pets, isLoading } = useGetPetList({ + status: StatusEnum.Available, + }) + + if (isLoading) return
Загрузка...
+ + return ( +
    + {pets?.map((pet) => ( +
  • {pet.name}
  • + ))} +
+ ) +} +``` + +Компонент не знает, что данные были запущены на сервере. Он использует обычный GET-хук REST-клиента. + +## Что важно + +- Ключ `fallback` должен совпадать с ключом GET-хука. +- `fallback` использует ту же key-функцию и те же params, что и GET-хук. +- Серверный код вызывает метод клиента, а не GET-хук. +- Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую. +- Эта стратегия не означает ручную работу с кешем в компонентах. + +## Когда не использовать + +Если данные нужны только серверному компоненту, используйте [Серверный await](/docs/applied/data-fetch/server-await). Если данные зависят от состояния браузера, используйте [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook). diff --git a/canons/style-guide/applied/data-fetch/index.md b/canons/style-guide/applied/data-fetch/index.md new file mode 100644 index 0000000..29d502d --- /dev/null +++ b/canons/style-guide/applied/data-fetch/index.md @@ -0,0 +1,100 @@ +--- +title: Получение данных +description: Как получать данные с учётом рендера страницы. +keywords: [rest, стратегии, render, isr, ssr, server components, swr, promise, business] +--- + +# Получение данных + +Как получать данные с учётом рендера страницы. + +Перед выбором стратегии должен быть настроен REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Настройка REST-клиента](/docs/applied/rest-client/setup/). + +## Сначала определите рендер страницы + +В Next.js выбор начинается не с `await`, `Suspense` или SWR. Сначала нужно понять, какой рендер получится у маршрута: static/ISR или dynamic/SSR. + +Next.js может перевести страницу в dynamic rendering автоматически, если в маршруте используются API текущего запроса. Поэтому первый вопрос такой: + +```text +Можно ли сохранить ISR, или странице нужны данные на каждый request? +``` + +ISR — приоритет. Если данные общие для пользователей и их можно обновлять с интервалом, не переводите страницу в SSR без необходимости. + +SSR/dynamic rendering выбирается только когда данные действительно зависят от текущего request или должны пересчитываться на каждый запрос. + +## Что переводит страницу в dynamic rendering + +Проверьте, нужны ли странице API и настройки, которые делают маршрут динамическим: + +- `cookies()` — данные зависят от cookie текущего пользователя. +- `headers()` — данные зависят от request headers. +- `draftMode()` — нужен preview/draft-режим. +- `searchParams` в `page.tsx` — данные зависят от query string. +- `cache: 'no-store'` или `revalidate: 0` в методе клиента — запрос нельзя кешировать. +- `connection()` — рендер явно ждёт request. +- `export const dynamic = 'force-dynamic'` — SSR включён вручную. + +Если ничего из этого не нужно, сначала проектируйте страницу как static/ISR. Серверный `await` сам по себе не означает SSR: режим зависит от кеширования запроса и dynamic API маршрута. + +## Рендер перед стратегией + +| Рендер | Когда подходит | Что выбирать дальше | +|--------|----------------|---------------------| +| Static/ISR | Данные общие и могут обновляться по интервалу | Серверные стратегии: `await`, `Promise.all`, передача промиса ниже, SWR `fallback` | +| SSR/dynamic | Данные зависят от request, пользователя или должны быть свежими на каждый запрос | Серверные стратегии с учётом блокировки первого HTML | +| После гидрации | Данные зависят от вкладки, фильтра, поиска, пагинации или действия пользователя | Клиентский GET-хук | + +## Как выбрать стратегию + +Когда режим рендера понятен, выбирайте конкретный способ получения данных: + +| Ситуация после выбора рендера | Стратегия | Где читать | +|-------------------------------|-----------|------------| +| Данные обязательны для первого HTML, SEO, `notFound()` или `redirect()` | Серверный `await` | [Серверный await](/docs/applied/data-fetch/server-await) | +| Несколько независимых данных нужны до рендера | Запуск промисов + `Promise.all` | [Параллельные серверные запросы](/docs/applied/data-fetch/parallel-server-requests) | +| Часть UI можно загрузить отдельно | Передача промиса ниже + `Suspense` | [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down) | +| Client Component должен получить данные сразу из SWR | Начальные данные для клиентских хуков | [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data) | +| Данные зависят от client state | Клиентский GET-хук | [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook) | +| Нужно объединить несколько запросов или вычислить `isAuth`, `canEdit`, `hasPets` | Business-композиция | [Business-композиция](/docs/applied/data-fetch/business-composition) | + +## Правило выбора + +Не выбирайте стратегию по любимому инструменту. Выбирайте её по двум вопросам: + +```text +Можно ли сохранить ISR? +Где нужны данные и что должно произойти до первого HTML? +``` + +Если данные можно кешировать между пользователями — сохраняйте static/ISR. Если данные request-specific — используйте SSR/dynamic rendering. Если данные зависят от состояния браузера — используйте GET-хук REST-клиента. Если простой GET превращается в доменный сценарий — переходите в `business/`. + +## Общие запреты + +```tsx +// Плохо — SSR включён на всякий случай +export const dynamic = 'force-dynamic' + +// Плохо — ISR отключён без требования к свежести на каждый request +export const revalidate = 0 + +// Плохо — прямой fetch в компоненте +useEffect(() => { + fetch('/api/pets').then(...) +}, []) + +// Плохо — useSWR в компоненте +const { data } = useSWR( + ['pet-store-api', '/pet/findByStatus?status=available'], + () => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }), +) + +// Плохо — бизнес-флаг внутри GET-хука REST-клиента +return { + ...query, + hasPets: Boolean(query.data?.length), +} +``` + +Не отключайте ISR без причины. В компонентах используются готовые методы клиента или готовые хуки. SWR-ключи, fetcher и транспорт остаются внутри REST-модуля. diff --git a/canons/style-guide/applied/data-fetch/parallel-server-requests.md b/canons/style-guide/applied/data-fetch/parallel-server-requests.md new file mode 100644 index 0000000..dfbef39 --- /dev/null +++ b/canons/style-guide/applied/data-fetch/parallel-server-requests.md @@ -0,0 +1,94 @@ +--- +title: Параллельные серверные запросы +description: Как запускать независимые REST-запросы на сервере без waterfall. +keywords: [rest, promise.all, параллельные запросы, server components] +--- + +# Параллельные серверные запросы + +Если серверному компоненту нужно несколько независимых данных, запускайте запросы до ожидания результата. Последовательный `await` создаёт waterfall и замедляет рендер. + +## Когда использовать + +- Запросы независимы друг от друга. +- Все данные нужны текущему серверному компоненту перед возвратом UI. +- Нельзя или не нужно стримить часть UI отдельно. + +## Хорошо + +```tsx +import { petStoreApi, StatusEnum } from 'infra/pet-store-api' +import { PetsDashboardScreen } from 'screens/pets-dashboard' + +export default async function PetsDashboardPage() { + const availablePetsPromise = petStoreApi.pet.findPetsByStatus({ + status: StatusEnum.Available, + }) + const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({ + status: StatusEnum.Pending, + }) + const soldPetsPromise = petStoreApi.pet.findPetsByStatus({ + status: StatusEnum.Sold, + }) + + const [availablePets, pendingPets, soldPets] = await Promise.all([ + availablePetsPromise, + pendingPetsPromise, + soldPetsPromise, + ]) + + return ( + + ) +} +``` + +## Плохо + +```tsx +export default async function PetsDashboardPage() { + const availablePets = await petStoreApi.pet.findPetsByStatus({ + status: StatusEnum.Available, + }) + const pendingPets = await petStoreApi.pet.findPetsByStatus({ + status: StatusEnum.Pending, + }) + const soldPets = await petStoreApi.pet.findPetsByStatus({ + status: StatusEnum.Sold, + }) + + return ( + + ) +} +``` + +Во втором примере каждый следующий запрос ждёт предыдущий, хотя они независимы. + +## Зависимые запросы + +Если второй запрос зависит от результата первого, последовательный `await` допустим: + +```tsx +export default async function OrderPage({ params }: OrderPageProps) { + const { id } = await params + const order = await petStoreApi.store.getOrderById({ orderId: Number(id) }) + const pet = await petStoreApi.pet.getPetById({ petId: order.petId }) + + return +} +``` + +Не превращайте зависимый сценарий в `Promise.all` искусственно. + +## Когда выбрать другую стратегию + +Если часть данных не обязательна для первого блока UI, можно запустить промис выше и передать его ниже: [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down). diff --git a/canons/style-guide/applied/data-fetch/pass-promise-down.md b/canons/style-guide/applied/data-fetch/pass-promise-down.md new file mode 100644 index 0000000..86f0964 --- /dev/null +++ b/canons/style-guide/applied/data-fetch/pass-promise-down.md @@ -0,0 +1,64 @@ +--- +title: Передача промиса ниже +description: Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте. +keywords: [rest, promise, suspense, streaming, server components] +--- + +# Передача промиса ниже + +Серверный компонент может запустить запрос и передать промис вложенному server-компоненту. Это полезно, когда часть UI можно загрузить отдельно через `Suspense`. + +## Когда использовать + +- Верхняя часть страницы может отрендериться без этих данных. +- Данные нужны только вложенному server-компоненту. +- Нужна `Suspense`-граница и серверный стриминг. + +## Пример + +```tsx +// src/app/(routes)/pets/page.tsx +import { Suspense } from 'react' +import { petStoreApi, StatusEnum } from 'infra/pet-store-api' +import { PetListSection } from 'widgets/pet-list-section' +import { PetListSkeleton } from 'widgets/pet-list-section' +import type { Pet } from 'infra/pet-store-api' + +export default function PetsPage() { + const petsPromise = petStoreApi.pet.findPetsByStatus({ + status: StatusEnum.Available, + }) + + return ( +
+

Питомцы

+ }> + + +
+ ) +} + +async function AvailablePets({ petsPromise }: { petsPromise: Promise }) { + const pets = await petsPromise + + return +} +``` + +Запрос стартует в `PetsPage`, но ожидание происходит внутри `AvailablePets`. `Suspense` управляет fallback для этой части UI. + +## Граница стратегии + +Эта стратегия остаётся серверной. Не используйте её как замену GET-хукам в Client Components. + +Если данные должны попасть в клиентский SWR-хук, используйте [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data). + +## Что не делать + +```tsx +// Плохо — передавать промис в произвольный клиентский компонент без ясной стратегии +return +``` + +Для клиентского потребления есть отдельная стратегия через `SWRConfig fallback` и готовые GET-хуки REST-клиента. diff --git a/canons/style-guide/applied/data-fetch/server-await.md b/canons/style-guide/applied/data-fetch/server-await.md new file mode 100644 index 0000000..4c2b17e --- /dev/null +++ b/canons/style-guide/applied/data-fetch/server-await.md @@ -0,0 +1,88 @@ +--- +title: Серверный await +description: Получение REST-данных на сервере до первого HTML. +keywords: [rest, server components, await, nextjs, isr, ssr, notFound, redirect] +--- + +# Серверный await + +Получение REST-данных на сервере до первого HTML. + +Серверный `await` — базовая стратегия для данных, которые нужны до рендера страницы или серверного блока. + +## Когда использовать + +- Данные нужны для первого HTML. +- Данные влияют на `metadata`. +- По результату запроса нужно вызвать `notFound()` или `redirect()`. +- Компонент серверный и данные не зависят от состояния браузера. + +## Влияние на рендер + +Серверный `await` сам по себе не означает SSR. В App Router страница может остаться static/ISR, если маршрут не использует dynamic API и запросы можно кешировать. + +ISR — приоритет для общих данных. Если список или детальная страница могут обновляться по интервалу, сохраняйте кеширование и не добавляйте `no-store`, `revalidate: 0` или `force-dynamic` без требования. + +SSR/dynamic rendering нужен, когда данные зависят от текущего request: cookie, headers, `searchParams`, preview-режим или персональные данные пользователя. + +## Пример страницы списка + +```tsx +// src/app/(routes)/pets/page.tsx +import { petStoreApi, StatusEnum } from 'infra/pet-store-api' +import { PetsScreen } from 'screens/pets' + +export default async function PetsPage() { + const pets = await petStoreApi.pet.findPetsByStatus({ + status: StatusEnum.Available, + }) + + return +} +``` + +`page.tsx` получает данные первого рендера и передаёт их ниже. UI страницы остаётся в `screens/`, а не пишется прямо в `app/`. + +## Пример детальной страницы + +```tsx +// src/app/(routes)/pets/[id]/page.tsx +import { notFound } from 'next/navigation' +import { petStoreApi } from 'infra/pet-store-api' +import { PetDetailScreen } from 'screens/pet-detail' + +type PetPageProps = { + params: Promise<{ id: string }> +} + +export default async function PetPage({ params }: PetPageProps) { + const { id } = await params + const pet = await petStoreApi.pet.getPetById({ petId: Number(id) }).catch(() => null) + + if (!pet) { + notFound() + } + + return +} +``` + +Обработка 404 зависит от API-клиента и класса ошибок. В примере показана идея: решение о `notFound()` принимается на уровне маршрута, а не внутри REST-клиента. + +## Что не делать + +```tsx +// Плохо — хуки нельзя вызывать в Server Component +const { data } = useGetPetList({ status: StatusEnum.Available }) + +// Плохо — прямой fetch в обход клиента +const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus') +``` + +Если данные нужны на сервере, вызывайте метод REST-клиента напрямую. + +## Когда выбрать другую стратегию + +- Несколько независимых запросов — [Параллельные серверные запросы](/docs/applied/data-fetch/parallel-server-requests). +- Часть UI можно грузить отдельно — [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down). +- Данные нужны клиентскому хуку сразу после гидрации — [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data). diff --git a/canons/style-guide/applied/fonts.md b/canons/style-guide/applied/fonts.md new file mode 100644 index 0000000..981276b --- /dev/null +++ b/canons/style-guide/applied/fonts.md @@ -0,0 +1,128 @@ +--- +title: Шрифты +description: Как подключать шрифты через Next.js Font в проекте. +--- + +# Шрифты + +Как подключать шрифты через Next.js Font в проекте. + +## Назначение + +Шрифты подключаются через `next/font`. Это стандартный способ Next.js: шрифты загружаются без ручных ``, `@font-face` и настройки preconnect. + +Шрифт подключается в точке инициализации приложения, а в CSS используется через переменную. + +## Google Fonts + +```tsx +// src/app/layout.tsx +import type { ReactNode } from 'react' +import { Inter } from 'next/font/google' +import 'shared/styles/global.css' + +const inter = Inter({ + subsets: ['latin', 'cyrillic'], + variable: '--font-main', + display: 'swap', +}) + +type RootLayoutProps = { + children: ReactNode +} + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + + {children} + + ) +} +``` + +```css +/* src/shared/styles/global.css */ +body { + font-family: var(--font-main), system-ui, sans-serif; +} +``` + +## Локальные шрифты + +Каждый локальный шрифт размещается в отдельной папке внутри `src/shared/fonts/`. В этой же папке лежит `.font.ts`, где объявляется `localFont`. + +```text +src/shared/fonts/ +└── roboto/ + ├── roboto.font.ts + ├── Roboto-Regular.woff2 + ├── Roboto-Italic.woff2 + ├── Roboto-Bold.woff2 + └── Roboto-BoldItalic.woff2 +``` + +```ts +// src/shared/fonts/roboto/roboto.font.ts +import localFont from 'next/font/local' + +export const roboto = localFont({ + src: [ + { + path: './Roboto-Regular.woff2', + weight: '400', + style: 'normal', + }, + { + path: './Roboto-Italic.woff2', + weight: '400', + style: 'italic', + }, + { + path: './Roboto-Bold.woff2', + weight: '700', + style: 'normal', + }, + { + path: './Roboto-BoldItalic.woff2', + weight: '700', + style: 'italic', + }, + ], + variable: '--font-main', + display: 'swap', +}) +``` + +`app/` импортирует готовый объект шрифта и только подключает его к документу: + +```tsx +// src/app/layout.tsx +import type { ReactNode } from 'react' +import { roboto } from 'shared/fonts/roboto/roboto.font' +import 'shared/styles/global.css' + +type RootLayoutProps = { + children: ReactNode +} + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + + {children} + + ) +} +``` + +Путь в `localFont` указывается относительно `.font.ts`, поэтому файлы шрифта импортируются коротко: `./Roboto-Regular.woff2`. + +Если шрифтов несколько, у каждого своя папка и свой `.font.ts`. + +## Правила + +- Использовать `next/font/google` или `next/font/local`. +- Не подключать шрифты через ручные `` и `@font-face` без необходимости. +- Подключать шрифты один раз — в корневом layout через готовый объект шрифта. +- Использовать CSS-переменные `variable`, а не жёстко прописывать семейство в каждом компоненте. +- Локальные файлы шрифтов хранить в `src/shared/fonts/{font-name}/` рядом с `{font-name}.font.ts`. +- Не объявлять `localFont` внутри `src/app/layout.tsx`; layout только импортирует готовый шрифт. diff --git a/canons/style-guide/applied/images.md b/canons/style-guide/applied/images.md new file mode 100644 index 0000000..c4b8be6 --- /dev/null +++ b/canons/style-guide/applied/images.md @@ -0,0 +1,95 @@ +--- +title: Изображения +description: Как подключать изображения через Next.js Image в проекте. +--- + +# Изображения + +Как подключать изображения через Next.js Image в проекте. + +## Назначение + +Изображения рендерятся через компонент `Image` из `next/image`. Это сохраняет единый API для размеров, `alt`, lazy-loading и `priority`, даже если оптимизация изображений отключена. + +В проекте оптимизация Next.js Image отключается через `unoptimized`, чтобы сборка и рантайм не зависели от встроенного image optimizer. + +## Настройка + +Отключение оптимизации задаётся глобально в `next.config.ts`: + +```ts +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + images: { + unoptimized: true, + }, +} + +export default nextConfig +``` + +После этого `unoptimized` не нужно повторять на каждом `Image`. + +## Использование + +Статические изображения, доступные по URL, размещаются в `public/`: + +```text +public/ +└── images/ + └── user-avatar.png +``` + +```tsx +import Image from 'next/image' + +export const UserAvatar = () => { + return ( + Аватар пользователя + ) +} +``` + +## Правила + +- Использовать `Image` из `next/image`, не обычный ``. +- Для контентных изображений всегда писать осмысленный `alt`. +- Для декоративных изображений использовать `alt=""`. +- Указывать `width` и `height`, если изображение не использует `fill`. +- При `fill` задавать `sizes` и контролировать размеры родителя стилями. +- `priority` ставить только для изображений первого экрана. +- SVG-иконки не оформлять как изображения — для них используется раздел [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-intro). + +## Пример с `fill` + +```tsx +import Image from 'next/image' +import styles from '../styles/article-card-cover.module.css' + +export const ArticleCardCover = () => { + return ( +
+ Обложка статьи +
+ ) +} +``` + +```css +.root { + position: relative; + aspect-ratio: 16 / 9; + overflow: hidden; +} +``` diff --git a/canons/style-guide/applied/localization.md b/canons/style-guide/applied/localization.md new file mode 100644 index 0000000..6b58703 --- /dev/null +++ b/canons/style-guide/applied/localization.md @@ -0,0 +1,81 @@ +--- +title: Локализация +description: Как организовать локализацию как infra-модуль. +--- + +# Локализация + +Как организовать локализацию как infra-модуль. + +## Назначение + +Локализация — инфраструктурная подсистема приложения. Она отвечает за текущую локаль, словари, форматирование переводов и API для компонентов. + +Код локализации живёт в `src/infra/i18n/`. Компоненты и модули не читают словари напрямую — они используют публичный API infra-модуля. + +## Структура + +```text +src/infra/i18n/ +├── config/ +│ └── i18n.config.ts +├── dictionaries/ +│ ├── ru.ts +│ └── en.ts +├── hooks/ +│ └── use-translation.hook.ts +├── providers/ +│ └── i18n-provider.tsx +├── types/ +│ └── i18n.type.ts +└── index.ts +``` + +Набор сегментов может отличаться, но публичная точка входа остаётся одна — `infra/i18n`. + +## Подключение + +`app/` только подключает готовый провайдер локализации. Реализация провайдера, словари и конфиг остаются в `infra/i18n/`. + +```tsx +// src/app/layout.tsx +import type { ReactNode } from 'react' +import { I18nProvider } from 'infra/i18n' + +type RootLayoutProps = { + children: ReactNode +} + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + + + {children} + + + ) +} +``` + +## Использование + +Компоненты получают переводы через готовый API модуля локализации: + +```tsx +import { useTranslation } from 'infra/i18n' + +export const ProfileTitle = () => { + const { t } = useTranslation() + + return

{t('profile.title')}

+} +``` + +## Правила + +- Локализация живёт в `infra/i18n/`. +- `app/` только подключает готовый provider и передаёт locale. +- Словари не импортируются напрямую в компоненты, screens или business-модули. +- Ключи переводов не собираются динамически из строк, если это ломает типизацию и поиск. +- Тексты интерфейса не хардкодятся в переиспользуемых компонентах, если они должны переводиться. +- Форматирование дат, чисел и валют должно проходить через API локализации или отдельные утилиты infra-модуля. diff --git a/canons/style-guide/applied/module.md b/canons/style-guide/applied/module.md new file mode 100644 index 0000000..94f21ac --- /dev/null +++ b/canons/style-guide/applied/module.md @@ -0,0 +1,156 @@ +--- +title: Модуль +description: Как должен выглядеть сгенерированный SLM-модуль в проекте. +--- + +# Модуль + +Как должен выглядеть сгенерированный SLM-модуль в проекте. + +## Назначение + +Архитектурное определение модуля описано в разделе [Архитектура → Модули](/docs/basics/architecture/modules). Список сегментов описан в разделе [Архитектура → Сегменты](/docs/basics/architecture/segments). + +Эта страница показывает прикладное оформление трёх типов модулей: UI, бизнес и инфраструктурный. + +## Создание + +1. Проверьте, что в проекте есть нужный шаблон в `.templates/`. +2. Если шаблона нет — создайте его по разделу [Создание шаблонов](/docs/applied/templates/templates-create). +3. Сгенерируйте модуль через [VS Code или CLI](/docs/applied/templates/templates-usage). + +## Типы модулей + +Архитектура определяет три типа модулей ([Типы модулей](/docs/basics/architecture/modules#типы-модулей)): + +| Тип | Обязательный файл | Описание | +|---|---|---| +| UI-модуль | `{name}.tsx` | Модуль, выросший из компонента | +| Бизнес-модуль | `{name}.factory.ts` | Модуль вокруг публичного runtime API | +| Инфраструктурный модуль | нет | Модуль вокруг технического сервиса | + +## UI-модуль + +UI-модуль — это компонент, который перерос ограничения компонента: получил собственные хуки, вложенные модули в `parts/`, сценарную логику или публичный API. Внутренняя структура та же, что у компонента: корневой `.tsx`, типы, стили, `ui/`. Но без ограничений компонента. + +Подробное оформление компонентов внутри `ui/` описано в разделе [Компонент](/docs/applied/component). + +## Бизнес-модуль + +Бизнес-модуль строится вокруг публичного runtime API. Ключевой файл — фабрика (`{name}.factory.ts`), которая возвращает всё, что нужно внешнему коду в runtime. + +Архитектурное описание фабрики: [Архитектура → Фабрика](/docs/basics/architecture/modules#фабрика). + +### Структура + +```text +business/customer/ +├── customer.factory.ts +├── index.ts +└── types/ + ├── customer.type.ts + ├── customer-api.type.ts + ├── customer-deps.type.ts + └── customer-factory.type.ts +``` + +### Типы + +`business/customer/types/customer-api.type.ts` + +```ts +export type CustomerApi = { + useCustomer: () => Customer + CustomerCard: (props: CustomerCardProps) => ReactNode +} +``` + +`business/order/types/order-deps.type.ts` + +```ts +export type OrderDeps = { + customer: Pick +} +``` + +`business/order/types/order-factory.type.ts` + +```ts +export type OrderFactory = (deps: OrderDeps) => OrderApi +``` + +### Фабрика без зависимостей + +`business/customer/customer.factory.ts` + +```ts +import type { CustomerFactory } from './types/customer-factory.type' + +export const customerFactory: CustomerFactory = () => { + return { + useCustomer, + CustomerCard, + } +} +``` + +### Фабрика с зависимостями + +`business/order/order.factory.ts` + +```ts +import type { OrderFactory } from './types/order-factory.type' + +export const orderFactory: OrderFactory = (deps) => { + return { + useOrder, + OrderCard, + } +} +``` + +### Композиция на уровне screen + +```tsx +// screens/home/home.screen.tsx +import { customerFactory } from '@/business/customer' +import { orderFactory } from '@/business/order' + +const customer = customerFactory() +const order = orderFactory({ customer }) + +const { useOrder, OrderCard } = order + +export const HomeScreen = () => { + const currentOrder = useOrder() + + return +} +``` + +## Инфраструктурный модуль + +Инфраструктурный модуль строится вокруг технического сервиса или интеграции. Его структура определяется природой сервиса — фиксированного корневого файла нет. + +Архитектурное описание: [Архитектура → Типы модулей → Инфраструктурный модуль](/docs/basics/architecture/modules#инфраструктурный-модуль). + +Пример модуля темы: + +```text +theme/ +├── index.ts +├── config/ +├── hooks/ +├── styles/ +└── ui/ +``` + +Пример модуля API-клиента: + +```text +backend-api/ +├── backend-api.client.ts +├── config/ +├── types/ +└── index.ts +``` diff --git a/canons/style-guide/applied/page-level.md b/canons/style-guide/applied/page-level.md new file mode 100644 index 0000000..14304a2 --- /dev/null +++ b/canons/style-guide/applied/page-level.md @@ -0,0 +1,186 @@ +--- +title: Файлы роутинга +description: Как работать со страницами и другими файлами роутинга Next.js App Router. +--- + +# Файлы роутинга + +Как работать со страницами и другими файлами роутинга Next.js App Router. + +## Назначение + +`src/app/**` — точка входа приложения и слой файлового роутинга Next.js. + +Файлы роутинга не реализуют интерфейс. Они описывают маршрут: читают параметры, получают данные первого рендера, подготавливают кеш или состояние и передают результат в screen. + +Границы слоя описаны в [Архитектура → Слои → App](/docs/basics/architecture/layers#слой-app). + +## Граница ответственности + +| Область | Где живёт | +|---|---| +| Файлы маршрутов (`page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`) | `src/app/**` | +| Параметры маршрута, `metadata`, `redirect()`, `notFound()` | `src/app/**` | +| Серверные запросы для первого рендера | `src/app/**`, через готовые клиенты и сервисы нижних слоёв | +| Прогрев SWR-кеша, начальное состояние, подключение провайдеров | `src/app/**`, только через готовые обёртки из нижних слоёв | +| UI страницы | `screens/` | +| Каркас страницы: header, footer, sidebar | `layouts/` | +| Провайдеры, сторы, хуки, API-клиенты, сервисы | нижние слои (`screens/`, `business/`, `infra/`, `shared/`) | +| CSS Modules и стили компонентов | рядом с компонентами, не в `src/app/**` | + +## Что можно делать в `page.tsx` + +- Экспортировать `metadata` или `generateMetadata`. +- Читать `params` и `searchParams`. +- Нормализовать и валидировать параметры маршрута. +- Делать серверные запросы для первого рендера через готовые клиенты или сервисы. +- Вызывать `redirect()` и `notFound()`. +- Готовить начальные данные для screen. +- Готовить SWR `fallback` и передавать его в готовый провайдер. +- Подключать готовый провайдер стора страницы и передавать начальное состояние. +- Рендерить screen или композицию из готовых обёрток и screen. + +## Что запрещено + +- Писать UI-разметку страницы прямо в файле роутинга. +- Создавать локальные компоненты внутри `src/app/**`. +- Добавлять CSS Modules, стили компонентов, `components/`, `styles/`, `hooks/`, `stores/`, `services/` внутри `src/app/**`. +- Реализовывать провайдеры, сторы, хуки, API-клиенты или сервисы в файлах роутинга. +- Размещать бизнес-логику, мапперы и правила предметной области в файлах роутинга. +- Вызывать `useSWR` и доменные клиентские хуки в файлах роутинга. + +## Страницы + +Страница объявляется через `export default function`. Для серверных запросов используется `async function`. + +```tsx +import type { Metadata } from 'next' +import { ProfileScreen } from 'screens/profile' + +export const metadata: Metadata = { + title: 'Профиль', + description: 'Страница профиля пользователя', +} + +type ProfilePageProps = { + params: Promise<{ id: string }> +} + +export default async function ProfilePage({ params }: ProfilePageProps) { + const { id } = await params + + return +} +``` + +## Данные первого рендера + +Если данные нужны до первого рендера, `page.tsx` получает их на сервере и передаёт в screen. Сам запрос выполняется через готовый клиент или сервис нижнего слоя. + +```tsx +import { notFound } from 'next/navigation' +import { userApi } from 'infra/backend-api' +import { UserScreen } from 'screens/user' + +type UserPageProps = { + params: Promise<{ id: string }> +} + +export default async function UserPage({ params }: UserPageProps) { + const { id } = await params + const user = await userApi.users.get(id) + + if (!user) { + notFound() + } + + return +} +``` + +Если данные нужны нескольким клиентским SWR-хукам, файл роутинга может обернуть дерево в `SWRConfig` и передать `fallback`. Запросы стартуют на сервере, а клиентские хуки получают данные из кеша. + +Ключи `fallback` должны совпадать с ключами внутри GET-хуков REST-клиента. Для array-key используется `unstable_serialize`. + +```tsx +import type { ReactNode } from 'react' +import { SWRConfig, unstable_serialize } from 'swr' +import { + backendApi, + getCurrentUserKey, + getPostListKey, +} from 'infra/backend-api' + +type FeedLayoutProps = { + children: ReactNode +} + +export default async function FeedLayout({ children }: FeedLayoutProps) { + const userPromise = backendApi.user.getCurrent() + const postsPromise = backendApi.posts.list() + + return ( + + {children} + + ) +} +``` + +Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [Получение данных](/docs/applied/data-fetch/), [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data). + +## Инициализация состояния + +Файл роутинга может подключить готовый провайдер стора страницы, если состояние зависит от маршрута или данных первого рендера. Реализация стора и провайдера не размещается в `src/app/**`. + +```tsx +import { ProfileScreen, ProfileStoreProvider } from 'screens/profile' + +type ProfilePageProps = { + params: Promise<{ id: string }> +} + +export default async function ProfilePage({ params }: ProfilePageProps) { + const { id } = await params + + return ( + + + + ) +} +``` + +## Layout + +`layout.tsx` подключает готовую инициализацию приложения: глобальные стили, провайдеры и верхнеуровневые обёртки из нижних слоёв. + +Вёрстка layout-каркаса выносится в слой `layouts/`. Реализация провайдеров, стилей и UI не размещается в `app/`. + +## Error и Not Found + +`error.tsx` и `not-found.tsx` делегируют разметку готовым screen или widget. В файле роутинга остаётся только адаптация API Next.js к пропсам нижнего слоя. + +```tsx +'use client' + +import { ErrorScreen } from 'screens/error' + +type ErrorPageProps = { + error: Error & { digest?: string } + reset: () => void +} + +const ErrorPage = ({ error, reset }: ErrorPageProps) => { + return +} + +export default ErrorPage +``` diff --git a/canons/style-guide/applied/postcss.md b/canons/style-guide/applied/postcss.md new file mode 100644 index 0000000..ec7c06f --- /dev/null +++ b/canons/style-guide/applied/postcss.md @@ -0,0 +1,70 @@ +--- +title: PostCSS +description: Установка и настройка CSS-процессора в новом проекте. +keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, autoprefixer, postcss-global-data, csstools, "@custom-media", "@nest", css-процессор] +--- + +# PostCSS + +Установка и настройка CSS-процессора в новом проекте. + +## Зачем PostCSS + +Подключаем ради двух вещей: + +- **Вложенность** — `&:hover`, `&::before`, `&._active` и `@media` внутри селектора. Без процессора нативный CSS не покрывает всех нужных кейсов вложенности. +- **`@custom-media`** — единые breakpoints проекта (`@media (--md)`) вместо магических `min-width`. Определяются в одном месте, переиспользуются везде. + +Autoprefixer и `@csstools/postcss-global-data` идут довеском под эти две задачи. + +## Требования + +- Next.js 14+ (App Router). +- Node.js 18+. + +CSS Modules поддерживаются Next.js из коробки — отдельной установки не требуют. + +## Установка + +1. Установить PostCSS-плагины как devDependencies: + + ```bash + npm install -D postcss-custom-media postcss-nesting autoprefixer @csstools/postcss-global-data + ``` + +2. Создать `postcss.config.mjs` в корне проекта (см. «Конфиг»). + +## Конфиг + +Файл `postcss.config.mjs` в корне проекта. + +```js +// postcss.config.mjs +export default { + plugins: { + '@csstools/postcss-global-data': { + files: ['src/shared/styles/media.css'], + }, + 'postcss-custom-media': {}, + 'postcss-nesting': {}, + autoprefixer: {}, + }, +} +``` + +### Разбор плагинов + +| Плагин | Назначение | +|--------|------------| +| `@csstools/postcss-global-data` | Подгружает определения `@custom-media` из `src/shared/styles/media.css` перед обработкой каждого CSS-модуля. Семантика — «глобальный файл определений, который не импортируется в исходники» | +| `postcss-custom-media` | Поддержка `@custom-media --md (...)` и использования `@media (--md) {}`. Определения берутся из файла, который подгрузил `postcss-global-data` | +| `postcss-nesting` | Нативная CSS-вложенность: `&:hover`, `&::before`, `&._active` | +| `autoprefixer` | Добавление вендорных префиксов по browserslist | + +### Почему внешний файл с `@custom-media`, а не `@import` + +`@custom-media` — глобальные определения, одинаковые для всего проекта. Держим их в `src/shared/styles/media.css`. `@csstools/postcss-global-data` подгружает этот файл перед каждым модулем, а `postcss-custom-media` заменяет `@media (--md)` на конкретные `@media (min-width: ...)` на этапе сборки. Сами определения в бандл не попадают. + +Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`. + +Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование стилей](/docs/applied/styles/styles-usage), раздел «Импорт стилей»). diff --git a/canons/style-guide/applied/project-structure.md b/canons/style-guide/applied/project-structure.md new file mode 100644 index 0000000..3262ae5 --- /dev/null +++ b/canons/style-guide/applied/project-structure.md @@ -0,0 +1,101 @@ +--- +title: Структура проекта +description: Из чего состоит проект и где что лежит. +--- + +# Структура проекта + +Из чего состоит проект и где что лежит. + +## Корень репозитория + +```text +project-root/ +├── .templates/ # Шаблоны для генерации модулей +├── .vscode/ # Настройки и рекомендуемые расширения VS Code +├── public/ # Статика, доступная по прямому URL +├── src/ # Исходный код приложения +├── .env.example # Переменные окружения проекта (шаблон) +├── .env # Переменные окружения проекта (не коммитить) +├── .gitignore +├── AGENTS.md # Инструкции для AI-агентов +├── biome.json # Линтер и форматтер (вместо ESLint + Prettier) +├── next.config.ts # Конфигурация Next.js +├── package.json # Зависимости и скрипты +├── postcss.config.mjs # Конфигурация PostCSS +└── tsconfig.json # Конфигурация TypeScript +``` + +## Папка `public/` + +Хранит статические файлы, которые отдаются по прямому URL без обработки сборщиком: + +```text +public/ +└── og-image.png +``` + +Компоненты, стили и другой исходный код здесь не размещаются. + +## Папка `src/` + +```text +src/ +├── app/ # Роутинг Next.js и точка входа приложения +├── layouts/ # Каркасы страниц (header, footer, sidebar) +├── screens/ # Контент конкретной страницы +├── widgets/ # Составные блоки интерфейса, не привязанные к домену +├── business/ # Бизнес-домены (auth, catalog, orders) +├── infra/ # Техсервисы (theme, i18n, API-адаптеры) +├── ui/ # UI-кит без бизнес-логики +└── shared/ # Общие ресурсы (утилиты, типы, стили) +``` + +Принципы организации слоёв описаны в разделе [Архитектура](../basics/architecture/). + +### Папка `app/` + +Точка входа приложения и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты). +`app/` подключает готовую инициализацию из нижних слоёв, но не реализует провайдеры, стили, UI-компоненты, хуки, сторы или сервисы. + +Подробнее о границах слоя: [Архитектура → Слои → App](/docs/basics/architecture/layers#слой-app). + +```text +src/app/ +├── layout.tsx # Корневой layout +└── page.tsx # Главная страница +``` + +## Папка `.templates/` + +Содержит шаблоны для генерации кода. Каждый подкаталог — шаблон отдельного типа модуля: + +```text +.templates/ +├── component/ # Шаблон компонента +├── screen/ # Шаблон экрана +├── layout/ # Шаблон layout +├── widget/ # Шаблон виджета +├── module/ # Шаблон бизнес-модуля +└── store/ # Шаблон стора +``` + +Подробнее о генерации описано в разделе [Шаблоны генерации](/docs/applied/templates/templates-intro). + +## Конфигурационные файлы + +| Файл | Назначение | +|---|---| +| `next.config.ts` | Настройки Next.js: редиректы, переменные окружения, webpack | +| `tsconfig.json` | Настройки TypeScript: пути, строгость, таргет | +| `biome.json` | Правила линтера и форматтера Biome | +| `postcss.config.mjs` | Подключение PostCSS-плагинов (CSS Modules, custom media) | +| `package.json` | Зависимости, версии, npm-скрипты | +| `AGENTS.md` | Инструкции для AI-агентов, работающих в проекте | + +## Переменные окружения + +- `.env` — переменные окружения проекта, запрещено коммитить +- `.env.example` — шаблон, коммитится в репозиторий + +Переменные с префиксом `NEXT_PUBLIC_` доступны в клиентском коде. Остальные доступны только на сервере. diff --git a/canons/style-guide/applied/rest-client/index.md b/canons/style-guide/applied/rest-client/index.md new file mode 100644 index 0000000..fd1cabc --- /dev/null +++ b/canons/style-guide/applied/rest-client/index.md @@ -0,0 +1,50 @@ +--- +title: REST-клиент +description: Настройка REST-клиента сервиса для работы с внешним API. +keywords: [rest, api, данные, infra, клиент, swr, стратегии] +--- + +# REST-клиент + +Настройка REST-клиента сервиса для работы с внешним API. + +## Настройка + +Для каждого внешнего сервиса создаётся отдельный API-клиент: `pet-store-api`, `billing-api`, `maps-api`. + +На этом этапе внешний API оформляется как модуль слоя `infra/`. + +Клиент отвечает за: + +- генерацию или ручное описание методов API; +- настройку `baseUrl`; +- заголовки и авторизацию; +- обработку ошибок; +- кастомизацию и расширение типов; +- GET-хуки для клиентских компонентов; +- прямое использование методов клиента в серверном коде и submit-функциях; +- публичный API модуля. + +Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную. + +GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента. + +Подробнее: + +- [Настройка REST-клиента](/docs/applied/rest-client/setup/) +- [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto) +- [Ручное создание](/docs/applied/rest-client/setup/manual) +- [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks) +- [Использование REST-клиента](/docs/applied/rest-client/usage) + +## Как читать раздел + +Если API ещё не подключён — начните с [Настройки REST-клиента](/docs/applied/rest-client/setup/). + +Если клиент уже создан и нужно вызвать его методы — откройте [Использование REST-клиента](/docs/applied/rest-client/usage). + +Если клиент уже есть, но непонятно как получить данные — начните с раздела [Получение данных](/docs/applied/data-fetch/). + +Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](/docs/applied/rest-client/setup/hooks). + +Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`. diff --git a/canons/style-guide/applied/rest-client/setup/auto.md b/canons/style-guide/applied/rest-client/setup/auto.md new file mode 100644 index 0000000..9b4573a --- /dev/null +++ b/canons/style-guide/applied/rest-client/setup/auto.md @@ -0,0 +1,272 @@ +--- +title: Автогенерация REST-клиента +description: Генерация REST-клиента из OpenAPI-спецификации. +keywords: [rest, openapi, api-codegen, автогенерация, generated, npx] +--- + +# Автогенерация REST-клиента + +Генерация REST-клиента из OpenAPI-спецификации. + +## Когда использовать + +Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки. + +## Пример API + +В примерах используется Swagger Petstore: + +```text +https://petstore3.swagger.io/api/v3/openapi.json +``` + +Имена модуля: + +```text +src/infra/pet-store-api/ +petStoreApi +pet-store-api.generated.ts +``` + +## Скрипт генерации + +`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию. + +```json +{ + "scripts": { + "codegen:pet-store-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infra/pet-store-api/generated -n pet-store-api.generated" + } +} +``` + +Параметры: + +- `-i` — путь к OpenAPI-спецификации: URL или локальный файл. +- `-o` — директория для сгенерированного файла. +- `-n` — имя сгенерированного файла без `.ts`. + +Ключ `--swr` не используется. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = один GET-метод, без бизнес-логики и композиции. + +## Генерация + +```bash +npm run codegen:pet-store-api +``` + +Ожидаемый результат: + +```text +src/infra/pet-store-api/generated/ +└── pet-store-api.generated.ts +``` + +Сгенерированный файл не правится руками и коммитится в репозиторий. + +## Проверка методов + +После генерации откройте `generated/pet-store-api.generated.ts` и проверьте фактические имена методов. + +Для Petstore нужны GET-операции вида: + +```ts +petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }) +petStoreApi.pet.getPetById({ petId: 10 }) +``` + +Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом. + +## Алгоритм для агента + +После генерации агент должен действовать по шагам: + +1. Открыть `generated/{service-name}.generated.ts`. +2. Найти фактические имена GET-методов клиента. +3. Для каждого нужного GET-метода найти generated-тип параметров и тип ответа. +4. Создать или обновить `client.ts` только для настройки транспорта и экспорта инстанса клиента. +5. Создать GET-хуки только для реально нужных GET-методов, не для всех методов API на всякий случай. +6. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`. +7. В key-функции вернуть `null`, если обязательные параметры не готовы. +8. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration`. +9. В fetcher вызвать generated-метод клиента с `params as GeneratedParams`. +10. Экспортировать хук и key-функцию из `hooks/index.ts`. +11. Экспортировать наружу только нужные generated-типы, generated enum, DTO и `hooks` через корневой `index.ts`. + +Что агент не должен делать: + +- Не использовать ключ `--swr` генератора. +- Не править `generated/*.generated.ts` руками. +- Не добавлять GET-хуки для POST, PUT, PATCH, DELETE. +- Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук. +- Не создавать словари enum-маппинга внутри GET-хука. +- Не объявлять DTO и response-типы в файле хука. +- Не вызывать `useSWR` условно. +- Не добавлять `throw` в fetcher для неготовых params. + +## `client.ts` + +Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента. + +```ts +// src/infra/pet-store-api/client.ts +import { Api, HttpClient } from './generated/pet-store-api.generated' + +const baseUrl = process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL + +if (!baseUrl) { + throw new Error('NEXT_PUBLIC_PET_STORE_API_BASE_URL is required') +} + +const httpClient = new HttpClient({ + baseUrl, + baseApiParams: { + secure: false, + headers: { + 'Content-Type': 'application/json', + }, + }, +}) + +export const petStoreApi = new Api(httpClient) +``` + +Локальное значение `NEXT_PUBLIC_PET_STORE_API_BASE_URL` задаётся в `.env.local`. Не добавляйте fallback вроде `?? 'http://localhost:8080/api/v3'` или `?? ''`: если env-переменная не задана, клиент должен падать с явной ошибкой конфигурации. + +`client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента. + +## GET-хуки + +GET-хуки пишутся вручную после проверки generated-методов. + +Пример для generated-метода `petStoreApi.pet.getPetById({ petId })`: + +```ts +// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts +import type { SWRConfiguration } from 'swr' +import useSWR from 'swr' +import { petStoreApi } from '../client' +import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated' + +export const getPetDetailKey = (params?: GetPetByIdParams | null) => { + if (!params?.petId) { + return null + } + + return ['pet-store-api', `/pet/${params.petId}`] as const +} + +/** + * Получает детальную карточку питомца с кешированием результата. + */ +export const useGetPetDetail = ( + params?: GetPetByIdParams | null, + config?: SWRConfiguration, +) => { + const key = getPetDetailKey(params) + const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams) + + return useSWR(key, fetcher, config) +} +``` + +Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks). + +## Расширение сгенерированных типов + +Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`. + +```text +src/infra/biocad-less-api/ +├── generated/ +│ └── biocad-less-api.generated.ts +├── types/ +│ ├── term.ts +│ └── index.ts +├── client.ts +└── index.ts +``` + +Пример расширения generated-типа: + +```ts +// src/infra/biocad-less-api/types/term.ts +import type { TermRecordItem } from '../generated/biocad-less-api.generated' + +declare module '../generated/biocad-less-api.generated' { + interface TermRecordItem { + media?: { + file?: string + title?: string + url?: string + } + } +} + +export type TermRecordItemExtended = Omit< + TermRecordItem, + 'categories' | 'tags' | 'fields' +> & { + categories?: Array<{ + _id?: string + id?: string + slug?: string + name?: string + }> + tags?: Array<{ + _id?: string + id?: string + slug?: string + name?: string + }> + fields?: Record +} +``` + +```ts +// src/infra/biocad-less-api/types/index.ts +export type { TermRecordItemExtended } from './term' +``` + +`declare module` используется для добавления отсутствующих полей в generated-интерфейс. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файл. + +## Публичный API + +```ts +// src/infra/pet-store-api/index.ts +export { petStoreApi } from './client' +export type { + FindPetsByStatusParams, + GetPetByIdParams, + Pet, +} from './generated/pet-store-api.generated' +export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated' +export * from './hooks' +``` + +Наружу импортируют только из `infra/pet-store-api`, не из `generated/`. + +Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`: + +```ts +// src/infra/biocad-less-api/index.ts +export type { TermRecordItemExtended } from './types' +``` + +## Регенерация + +При изменении OpenAPI-схемы: + +```bash +npm run codegen:pet-store-api +``` + +Что меняется: + +- `generated/pet-store-api.generated.ts` — перезаписывается генератором. +- `client.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически. + +Если после регенерации поменялись сигнатуры методов или типы, это исправляется в ручном коде модуля. + +## Следующий шаг + +После генерации и настройки `client.ts` проверьте [использование REST-клиента](/docs/applied/rest-client/usage) или добавьте [GET-хук REST-клиента](/docs/applied/rest-client/setup/hooks) для Client Components. diff --git a/canons/style-guide/applied/rest-client/setup/hooks.md b/canons/style-guide/applied/rest-client/setup/hooks.md new file mode 100644 index 0000000..3f830c3 --- /dev/null +++ b/canons/style-guide/applied/rest-client/setup/hooks.md @@ -0,0 +1,313 @@ +--- +title: GET-хуки REST-клиента +description: Прозрачные SWR-обёртки над GET-методами REST-клиента. +keywords: [rest, swr, get-хуки, client components, infra] +--- + +# GET-хуки REST-клиента + +Прозрачные SWR-обёртки над GET-методами REST-клиента. + +## Зачем нужны + +GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с `useSWR`, ключами кеша и fetcher напрямую. + +## Где лежат + +GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним: + +```text +src/infra/ +└── pet-store-api/ + ├── client.ts + ├── generated/ + ├── hooks/ + │ ├── use-get-pet-list.hook.ts + │ ├── use-get-pet-detail.hook.ts + │ └── index.ts + ├── types/ + └── index.ts +``` + +## Контракт + +- Один GET-хук = один GET-метод клиента. +- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`. +- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`. +- Хук принимает `params?: GeneratedParams | null` и `config?: SWRConfiguration`. +- Для GET-метода без параметров хук принимает только `config?: SWRConfiguration`. +- Key-функция принимает те же `params`, что и хук. +- Key-функция возвращает `null`, если обязательные параметры не готовы. +- Проверка готовности запроса живёт в key-функции, а не в теле хука. +- Хук вызывает `useSWR` один раз и безусловно. +- Fetcher не проверяет `null`, не бросает ошибку и не вызывает метод клиента с `null`. +- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`. +- Хук возвращает тип ответа API: generated-тип или DTO из `types/`. +- Хук не объединяет несколько запросов. +- Хук не маппит DTO в доменную модель. +- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`. +- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние. + +## Формат SWR-ключа + +SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией. + +Формат ключа: + +```ts +['pet-store-api', '/pet/10'] as const +``` + +- Первый элемент — имя API-сервиса или REST-клиента в `kebab-case`. +- Второй элемент — endpoint запроса: path и query string. +- Key-функция возвращает `null`, когда запрос нельзя выполнять. +- Key-функция нужна и GET-хуку, и `SWRConfig fallback`. +- Не используйте произвольные части вроде `['pet-store-api', 'pet', 'detail', params]`. +- Не используйте только строку endpoint без имени сервиса. + +Примеры ключей: + +```ts +export const getPetDetailKey = (params?: GetPetByIdParams | null) => { + if (!params?.petId) { + return null + } + + return ['pet-store-api', `/pet/${params.petId}`] as const +} +``` + +```ts +export const getPetListKey = (params?: FindPetsByStatusParams | null) => { + if (!params?.status) { + return null + } + + return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const +} +``` + +```ts +export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => { + if (!params?.tags.length) { + return null + } + + return ['pet-store-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const +} +``` + +Если API допускает `0` как валидный идентификатор, не используйте проверку `!params?.id`. В таком случае проверяйте `null` и `undefined` явно. + +## Пример списка + +```ts +// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts +import type { SWRConfiguration } from 'swr' +import useSWR from 'swr' +import { petStoreApi } from '../client' +import type { + FindPetsByStatusParams, + Pet, +} from '../generated/pet-store-api.generated' + +export const getPetListKey = (params?: FindPetsByStatusParams | null) => { + if (!params?.status) { + return null + } + + return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const +} + +/** + * Получает список питомцев по статусу. + */ +export const useGetPetList = ( + params?: FindPetsByStatusParams | null, + config?: SWRConfiguration, +) => { + const key = getPetListKey(params) + const fetcher = () => petStoreApi.pet.findPetsByStatus( + params as FindPetsByStatusParams, + ) + + return useSWR(key, fetcher, config) +} +``` + +`params as FindPetsByStatusParams` допустим только в fetcher: готовность параметров проверена в key-функции, а при `key = null` SWR не вызывает fetcher. + +## Пример detail-запроса + +```ts +// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts +import type { SWRConfiguration } from 'swr' +import useSWR from 'swr' +import { petStoreApi } from '../client' +import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated' + +export const getPetDetailKey = (params?: GetPetByIdParams | null) => { + if (!params?.petId) { + return null + } + + return ['pet-store-api', `/pet/${params.petId}`] as const +} + +/** + * Получает детальную карточку питомца с кешированием результата. + */ +export const useGetPetDetail = ( + params?: GetPetByIdParams | null, + config?: SWRConfiguration, +) => { + const key = getPetDetailKey(params) + const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams) + + return useSWR(key, fetcher, config) +} +``` + +## Пример без параметров + +```ts +// src/infra/pet-store-api/hooks/use-get-store-inventory.hook.ts +import type { SWRConfiguration } from 'swr' +import useSWR from 'swr' +import { petStoreApi } from '../client' +import type { StoreInventory } from '../types' + +export const getStoreInventoryKey = () => { + return ['pet-store-api', '/store/inventory'] as const +} + +/** + * Получает инвентарь магазина. + */ +export const useGetStoreInventory = ( + config?: SWRConfiguration, +) => { + return useSWR( + getStoreInventoryKey(), + () => petStoreApi.store.getInventory(), + config, + ) +} +``` + +Если generated-метод возвращает безымянный тип вроде `Record`, а тип нужен наружу, вынесите его в `types/`. + +## Отложенный запрос + +GET-хук может принимать `null` или `undefined` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя. + +```ts +const key = getPetDetailKey(params) +``` + +Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа. + +Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`. + +## Экспорт + +```ts +// src/infra/pet-store-api/hooks/index.ts +export { getPetListKey, useGetPetList } from './use-get-pet-list.hook' +export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook' +export { + getStoreInventoryKey, + useGetStoreInventory, +} from './use-get-store-inventory.hook' +``` + +```ts +// src/infra/pet-store-api/index.ts +export { petStoreApi } from './client' +export type { + FindPetsByStatusParams, + GetPetByIdParams, + Pet, +} from './generated/pet-store-api.generated' +export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated' +export * from './hooks' +export type { StoreInventory } from './types' +``` + +Наружу импортируют только из `infra/pet-store-api`, не из `generated/` и не из `hooks/` напрямую. + +## Где заканчивается infra + +```ts +// Хорошо: infra, прозрачный GET-хук +const { data: pets } = useGetPetList({ status: StatusEnum.Available }) +``` + +```ts +// Хорошо: business, доменная интерпретация +export const useAvailablePets = () => { + const query = useGetPetList({ status: StatusEnum.Available }) + + return { + ...query, + hasPets: Boolean(query.data?.length), + } +} +``` + +`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`. + +## Что запрещено + +```ts +// Плохо — useSWR в компоненте +const { data } = useSWR( + ['pet-store-api', '/pet/findByStatus?status=available'], + () => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }), +) + +// Плохо — проверка готовности размазана по хуку +export const useGetPetDetail = (params?: GetPetByIdParams | null) => { + const key = params?.petId ? getPetDetailKey(params) : null + const fetcher = () => { + if (!params?.petId) { + throw new Error('Pet id is required') + } + + return petStoreApi.pet.getPetById(params) + } + + return useSWR(key, fetcher) +} + +// Плохо — условный вызов useSWR нарушает rules of hooks +export const useGetPetDetail = (params?: GetPetByIdParams | null) => { + const key = getPetDetailKey(params) + + if (key === null) { + return useSWR(null, null) + } + + return useSWR(key, () => petStoreApi.pet.getPetById(params)) +} + +// Плохо — несколько GET внутри infra-хука +export const usePetDashboard = () => { + const available = useGetPetList({ status: StatusEnum.Available }) + const sold = useGetPetList({ status: StatusEnum.Sold }) + + return { available, sold } +} + +// Плохо — бизнес-флаг внутри GET-хука REST-клиента +export const useGetPetList = (params?: FindPetsByStatusParams | null) => { + const query = useSWR(...) + + return { + ...query, + hasPets: Boolean(query.data?.length), + } +} +``` + +Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook). diff --git a/canons/style-guide/applied/rest-client/setup/index.md b/canons/style-guide/applied/rest-client/setup/index.md new file mode 100644 index 0000000..8aaf022 --- /dev/null +++ b/canons/style-guide/applied/rest-client/setup/index.md @@ -0,0 +1,88 @@ +--- +title: Настройка REST-клиента +description: Подготовка REST-клиента сервиса к использованию. +keywords: [rest, клиент, infra, методы, openapi, get-хуки, swr] +--- + +# Настройка REST-клиента + +Подготовка REST-клиента сервиса к использованию. + +## Что настраиваем + +REST-клиент — это infra-модуль, через который проект работает с внешним REST API. + +На этапе настройки нужно подготовить клиент сервиса: оболочку клиента, методы API и GET-хуки для клиентских компонентов. + +## Из чего состоит клиент + +REST-клиент состоит из трёх основных частей: + +1. **Клиент** — самописная оболочка над транспортом. +2. **Методы** — сгенерированные из OpenAPI или написанные вручную вызовы API. +3. **GET-хуки** — SWR-обёртки для GET-запросов. + +Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису. + +## Клиент + +Клиент — ручной слой, который настраивает работу с API: `baseUrl`, заголовки, авторизацию, обработку ошибок и создание инстанса сервиса. + +Даже если методы генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта. + +`client.ts` — только сборочная точка клиента. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логика. + +`baseUrl` API задаётся обязательной env-переменной без fallback-значения в коде. Не используйте записи вроде `process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL ?? 'http://localhost:8080/api/v3'` или `?? ''`: локальный URL должен лежать в `.env.local`, а отсутствие переменной должно приводить к явной ошибке конфигурации. + +## Методы + +Методы описывают конкретные запросы к API. + +Они появляются одним из двух способов: + +- генерируются из OpenAPI в `generated/`; +- создаются вручную в `methods/`. + +Подробности: + +- [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto) +- [Ручное создание](/docs/applied/rest-client/setup/manual) + +## GET-хуки + +Для GET-запросов добавляются GET-хуки REST-клиента. + +Это прозрачные SWR-обёртки над GET-методами клиента. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components. + +GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`. + +Каждый GET-хук имеет экспортируемую key-функцию. SWR-ключ всегда имеет формат `[serviceName, endpoint]`: например `['pet-store-api', '/pet/10']`. + +Хук принимает generated-параметры метода и SWR-настройки: `params?: GetPetByIdParams | null`, `config?: SWRConfiguration`. + +Подробности: + +- [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks) + +## Структура модуля + +```text +src/infra/{service-name}/ +├── client.ts # самописная оболочка и инстанс клиента +├── generated/ или methods/ # методы API +├── hooks/ # GET-хуки REST-клиента +├── types/ # DTO, именованные response-типы и расширения типов +├── errors/ # ошибки API, если нужны +└── index.ts # публичный API +``` + +`index.ts` — единственная точка входа в REST-модуль для внешнего кода. + +Если generated-метод возвращает безымянный тип вроде `Record`, а этот тип нужен снаружи, вынесите его в `types/`. Не объявляйте DTO внутри `hooks/use-get-*.hook.ts`. + +## Что делаем дальше + +1. Создайте методы клиента: [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto) или [Ручное создание](/docs/applied/rest-client/setup/manual). +2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks). +3. Проверьте прямые вызовы клиента: [Использование REST-клиента](/docs/applied/rest-client/usage). +4. После настройки клиента переходите к [Получению данных](/docs/applied/data-fetch/). diff --git a/canons/style-guide/applied/rest-client/setup/manual.md b/canons/style-guide/applied/rest-client/setup/manual.md new file mode 100644 index 0000000..4d87529 --- /dev/null +++ b/canons/style-guide/applied/rest-client/setup/manual.md @@ -0,0 +1,198 @@ +--- +title: Ручное создание REST-клиента +description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный. +keywords: [rest, ручной клиент, fetch, methods, dto, errors, infra] +--- + +# Ручное создание REST-клиента + +Создание REST-клиента вручную, когда OpenAPI нет или он неполный. + +## Когда использовать + +Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации. + +Задача ручного клиента — дать такую же точку входа, как у автогенерированного клиента: именованный API-объект, методы по сущностям, DTO и GET-хуки рядом с клиентом. + +## Что нужно создать + +```text +src/infra/ +└── pet-project-api/ + ├── methods/ + │ └── posts.ts + ├── hooks/ + │ └── index.ts + ├── types/ + │ ├── client.ts + │ ├── post.ts + │ └── index.ts + ├── errors/ + │ └── pet-project-api.error.ts + ├── client.ts + └── index.ts +``` + +| Файл | Роль | +|------|------| +| `client.ts` | Базовый транспорт и создание инстанса клиента | +| `methods/` | Методы API по сущностям | +| `types/` | DTO запросов, ответов и типы клиента | +| `errors/` | Ошибки конкретного API | +| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components | +| `index.ts` | Публичный API REST-модуля | + +## DTO и типы API + +DTO запросов и ответов живут в `types/`. `client.ts` не содержит DTO и доменные типы. + +```ts +// src/infra/pet-project-api/types/post.ts +export type PostDto = { + id: string + slug: string + title: string +} + +export type PostListQueryDto = { + limit?: number + category?: string +} +``` + +```ts +// src/infra/pet-project-api/types/index.ts +export type { PostDto, PostListQueryDto } from './post' +``` + +Типы, которые нужны только базовому транспорту, можно держать отдельно: + +```ts +// src/infra/pet-project-api/types/client.ts +export type QueryParams = Record +``` + +## Ошибка API + +Ошибка API тоже относится к REST-модулю. + +```ts +// src/infra/pet-project-api/errors/pet-project-api.error.ts +export class PetProjectApiError extends Error { + constructor( + public readonly status: number, + message: string, + ) { + super(message) + this.name = 'PetProjectApiError' + } +} +``` + +## Базовый клиент + +`client.ts` содержит только транспортную оболочку и сборку инстанса. Прямой `fetch` живёт здесь, а не в компонентах и не в методах верхних слоёв. + +```ts +// src/infra/pet-project-api/client.ts +import { PetProjectApiError } from './errors/pet-project-api.error' +import type { QueryParams } from './types/client' + +export class PetProjectApiClient { + constructor( + private readonly baseUrl: string, + private readonly defaultHeaders: Record = {}, + ) {} + + async get(path: string, params: QueryParams = {}): Promise { + const base = `${this.baseUrl.replace(/\/+$/, '')}/` + const url = new URL(path.replace(/^\/+/, ''), base) + + Object.entries(params).forEach(([key, value]) => { + url.searchParams.set(key, String(value)) + }) + + const response = await fetch(url, { + headers: { + Accept: 'application/json', + ...this.defaultHeaders, + }, + }) + + if (!response.ok) { + throw new PetProjectApiError(response.status, response.statusText) + } + + return response.json() as Promise + } +} +``` + +Это минимальный шаблон. Авторизация, дополнительные заголовки, `next.revalidate`, `post`, `formdata` и другие детали добавляются только когда они реально нужны API. + +## Методы API + +Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI. + +```ts +// src/infra/pet-project-api/methods/posts.ts +import type { PetProjectApiClient } from '../client' +import type { PostDto, PostListQueryDto } from '../types/post' + +export function postsMethods(client: PetProjectApiClient) { + return { + /** GET /posts */ + list: (query: PostListQueryDto = {}) => + client.get('posts', query), + + /** GET /posts/{slug} */ + get: (slug: string) => + client.get(`posts/${slug}`), + } +} +``` + +Метод возвращает DTO в форме API. Если данным нужен доменный смысл, маппинг делается выше, в `business/`. + +## Публичный API + +`index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля. + +```ts +// src/infra/pet-project-api/index.ts +import { PetProjectApiClient } from './client' +import { postsMethods } from './methods/posts' + +const baseUrl = process.env.NEXT_PUBLIC_PET_PROJECT_API_BASE_URL + +if (!baseUrl) { + throw new Error('NEXT_PUBLIC_PET_PROJECT_API_BASE_URL is required') +} + +const client = new PetProjectApiClient( + baseUrl, + { 'Content-Type': 'application/json' }, +) + +export const petProjectApi = { + posts: postsMethods(client), +} + +export { PetProjectApiError } from './errors/pet-project-api.error' +export type { PostDto, PostListQueryDto } from './types' +export * from './hooks' +``` + +Внешний код импортирует только из `infra/pet-project-api`, не из внутренних файлов модуля. + +## Правила + +- `fetch` используется только внутри базового клиента. +- DTO запросов и ответов живут в `types/`. +- `client.ts` не содержит DTO, GET-хуки и бизнес-логику. +- `baseUrl` берётся из обязательной env-переменной без fallback-значения в коде. +- Методы лежат в `methods/` и возвращают DTO. +- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components. +- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`. + +Следующий шаг: [Использование REST-клиента](/docs/applied/rest-client/usage), [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks) или [Получение данных](/docs/applied/data-fetch/). diff --git a/canons/style-guide/applied/rest-client/usage.md b/canons/style-guide/applied/rest-client/usage.md new file mode 100644 index 0000000..d4b2097 --- /dev/null +++ b/canons/style-guide/applied/rest-client/usage.md @@ -0,0 +1,21 @@ +--- +title: Использование REST-клиента +description: Как вызвать готовый REST-клиент в функции. +keywords: [rest, api client, submit, generated, pet-store-api] +--- + +# Использование REST-клиента + +Как вызвать готовый REST-клиент в функции. + +## Пример + +```ts +import { petStoreApi } from 'infra/pet-store-api' + +export const getPet = async (petId: number) => { + const pet = await petStoreApi.pet.getPetById({ petId }) + + console.log(pet) +} +``` diff --git a/canons/style-guide/applied/stores.md b/canons/style-guide/applied/stores.md new file mode 100644 index 0000000..e69de29 diff --git a/canons/style-guide/applied/styles/styles-setup.md b/canons/style-guide/applied/styles/styles-setup.md new file mode 100644 index 0000000..ce9b45c --- /dev/null +++ b/canons/style-guide/applied/styles/styles-setup.md @@ -0,0 +1,176 @@ +--- +title: Настройка стилей +description: "Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили." +keywords: [variables.css, media.css, global.css, shared/styles, токены, переменные, custom-media, breakpoints, подключение стилей, базовые стили, инициализация] +--- + +# Настройка стилей + +Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили. + +## Требования + +- Установлен PostCSS или любой другой pre/post-процессор с поддержкой `@custom-media`. + +## Файлы + +Состав глобальных стилей — три файла: + +| Файл | Роль | +|------|------| +| `variables.css` | Токены проекта (цвета, отступы, радиусы) | +| `media.css` | Custom media queries (брейкпоинты по ширине и высоте) | +| `global.css` | Точка сборки глобальных стилей: через `@import` тянет все остальные глобалы, импортируется в приложение один раз | + +Правила подключения: + +- В приложение импортируется **только** `global.css`. +- `variables.css` и будущие глобальные файлы (резеты, темы, типографика) подключаются в `global.css` через `@import`. +- `media.css` **не импортируется** — ни в `global.css`, ни в компоненты, ни в точку инициализации. Его читает CSS-процессор на этапе сборки (см. [PostCSS](/docs/applied/postcss)). + +## Корневой `font-size` + +Базовая единица `rem` в проекте привязана к **16px**: корневой `font-size` не переопределяется. `html { font-size: ... }` писать запрещено — пользовательская настройка размера шрифта в браузере должна работать (a11y). Все `rem`-значения в `media.css` и других стилях трактуются как `1rem = 16px по умолчанию`. + +Reset браузерных дефолтов (`box-sizing`, сброс `margin`, типографика) каноном не задаётся — каждый проект решает сам. Если заводится — подключается через `global.css`. + +## Установка + +### 1. Создать файлы + +```bash +mkdir -p src/shared/styles +touch src/shared/styles/variables.css src/shared/styles/media.css src/shared/styles/global.css +``` + +### 2. Заполнить `media.css` + +Файл `src/shared/styles/media.css`. Стандартный набор брейкпоинтов проекта; редактировать только при согласованном изменении шкалы. + +Единица — `rem` (реагирует на корневой `font-size`). Перевод исходит из дефолтного `html { font-size: 16px }`, т.е. `1rem = 16px`. + +```css +/* src/shared/styles/media.css */ + +/* Ширина — Mobile First (min-width), кроме --xs (max-width) */ +@custom-media --xs (max-width: 35.9375rem); /* 575px — до sm */ +@custom-media --sm (min-width: 36rem); /* 576px — телефон альбом / малый планшет */ +@custom-media --md (min-width: 48rem); /* 768px — планшет */ +@custom-media --lg (min-width: 62rem); /* 992px — малый десктоп */ +@custom-media --xl (min-width: 75rem); /* 1200px — десктоп */ +@custom-media --2xl (min-width: 88rem); /* 1408px — широкий десктоп */ +@custom-media --3xl (min-width: 120rem); /* 1920px — full HD+ */ + +/* Высота — min-height */ +@custom-media --h-xs (min-height: 41.6875rem); /* 667px — iPhone SE портрет */ +@custom-media --h-sm (min-height: 43.875rem); /* 702px */ +@custom-media --h-md (min-height: 50.625rem); /* 810px — iPad портрет */ +@custom-media --h-lg (min-height: 56.25rem); /* 900px */ +@custom-media --h-xl (min-height: 62.5rem); /* 1000px */ +@custom-media --h-2xl (min-height: 68.75rem); /* 1100px */ +@custom-media --h-3xl (min-height: 75rem); /* 1200px */ +``` + +Правила: + +- только `@custom-media` на верхнем уровне; +- имена короткие, по шкале (`--xs` … `--3xl`); высотные — с префиксом `--h-`; +- единица — `rem`, не `em`/`px`; пиксельное значение указывается комментарием; +- значения ширины — `min-width` (Mobile First), исключение `--xs` — `max-width` (блок «строго меньше `--sm`»); +- значения высоты — `min-height`. + +### 3. Заполнить `variables.css` + +Файл `src/shared/styles/variables.css`. Набор токенов под проект расширяется по мере роста дизайн-системы. + +```css +/* src/shared/styles/variables.css */ +:root { + /* Цвета */ + --color-primary: #3b82f6; + --color-bg: #ffffff; + --color-bg-hover: #f5f5f5; + --color-text: #1a1a1a; + + /* Отступы */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + + /* Скругления */ + --radius-1: 4px; + --radius-2: 8px; +} +``` + +Правила: + +- все токены определяются в `:root` — без вложенных селекторов; +- именование — `kebab-case` по ролям: `--color-*`, `--space-*`, `--radius-*`; +- `px` — основная единица для пространственных токенов; +- темы накладываются поверх через `[data-theme="..."] { ... }` — в отдельном файле темы или здесь же. + +`variables.css` напрямую в приложение не импортируется — только через `global.css`. + +### 4. Заполнить `global.css` + +Файл `src/shared/styles/global.css`. Единственный глобальный файл, импортируемый в точку инициализации приложения. Внутри — `@import` остальных глобалов относительным путём. + +```css +/* src/shared/styles/global.css */ +@import './variables.css'; + +/* Сюда же подключаются будущие глобалы через @import: + * @import './reset.css'; + * @import './typography.css'; + * @import './themes.css'; + * media.css НЕ импортируется — он работает через PostCSS. + */ +``` + +Правила: + +- пути в `@import` — относительные (`./variables.css`), не через алиасы; нативный CSS `@import` не понимает tsconfig-paths; +- `media.css` в `global.css` **не импортируется**; +- собственные глобальные правила (`html { ... }`, `body { ... }`) писать **не здесь**, а в отдельных файлах рядом (`reset.css`, `typography.css`) и подключать через `@import`. `global.css` — только точка сборки; +- порядок `@import` определяет порядок каскада: токены первыми, дальше резеты / темы / типографика. + +### 5. Подключить `global.css` в layout + +Импорт делается **один раз** — в корневом layout приложения: + +```tsx +// src/app/layout.tsx +import 'shared/styles/global.css' + +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'App', + description: '', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} +``` + +`variables.css` и `media.css` в layout **не импортируются напрямую** — только через `global.css` (variables) или через PostCSS на сборке (media). + +## Проверка установки + +- В `src/shared/styles/` присутствуют три файла: `variables.css`, `media.css`, `global.css`. В `src/app/` папки `styles/` нет. +- В `src/app/layout.tsx` есть `import 'shared/styles/global.css'`. Импортов `variables.css` и `media.css` там нет. +- В проекте **не появились** PostCSS-пакеты и `postcss.config.*` — этот раздел их не ставит. +- `npm run build` завершается успешно. + +## Дальше + +- [PostCSS](/docs/applied/postcss) — подключить процессор, чтобы заработали `@media (--md)` и вложенность. +- [Использование стилей](/docs/applied/styles/styles-usage) — правила написания CSS в компонентах. +- [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-setup) — стили иконок отдельно от глобальных. diff --git a/canons/style-guide/applied/styles/styles-usage.md b/canons/style-guide/applied/styles/styles-usage.md new file mode 100644 index 0000000..d7f1405 --- /dev/null +++ b/canons/style-guide/applied/styles/styles-usage.md @@ -0,0 +1,271 @@ +--- +title: Использование стилей +description: Как пишутся стили в проекте. +--- + +# Использование стилей + +Как пишутся стили в проекте. + +## Общие правила + +- Только **PostCSS** и **CSS Modules** для кастомной стилизации. +- Подход **Mobile First** — стили пишутся от мобильных к десктопу. +- Именование классов — `camelCase` (`.root`, `.buttonNext`, `.itemTitle`). +- Корневой класс каждого CSS Module компонента всегда называется `.root` — это упрощает ориентацию в DevTools и отладку DOM. +- Модификаторы — отдельный класс с `_`, применяется через `&._modifier`. + +**Хорошо** +```css +.submitButton { + padding: 8px 16px; + + &._disabled { + opacity: 0.5; + } +} +``` + +**Плохо** +```css +/* Плохо: kebab-case и вложенный элемент вместо отдельного класса. */ +.submit-button { + padding: 8px 16px; + + &__icon { + margin-right: 8px; + } +} +``` + +## Вложенность + +- Вложенность селекторов запрещена. +- Исключения: + - Псевдоклассы: `&:hover`, `&:active`, `&:focus`, `&:disabled` и т.д. + - Псевдоэлементы: `&::before`, `&::after`. + - Медиа-запросы: `@media`. + - Модификаторы: `&._active`, `&._disabled`. +- Каждый вложенный блок отделяется пустой строкой от предыдущих свойств. + +**Хорошо** +```css +.card { + padding: 16px; + background-color: var(--color-bg); + + &:hover { + background-color: var(--color-bg-hover); + } + + &::after { + content: ''; + display: block; + } + + &._highlighted { + border-color: var(--color-primary); + } + + @media (--md) { + padding: 24px; + } +} + +.cardTitle { + font-size: 16px; + + @media (--md) { + font-size: 20px; + } +} +``` + +**Плохо** +```css +/* Плохо: вложенность селекторов, нет пустых строк между блоками. */ +.card { + padding: 16px; + .cardTitle { + font-size: 16px; + } + &:hover { + background-color: var(--color-bg-hover); + } +} +``` + +## Медиа-запросы + +- Только **Custom Media Queries**: `@media (--md) {}`. +- Запрещены произвольные breakpoints: `@media (min-width: 768px)`. +- `@media` пишется только **внутри** селектора. +- Запрещено писать `@media` на верхнем уровне с селекторами внутри. + +**Хорошо** +```css +.sidebar { + display: none; + + @media (--md) { + display: block; + } +} + +.sidebarTitle { + font-size: 14px; + + @media (--md) { + font-size: 18px; + } +} +``` + +**Плохо** +```css +/* Плохо: @media на верхнем уровне с селекторами внутри. */ +@media (--md) { + .sidebar { + display: block; + } + + .sidebarTitle { + font-size: 18px; + } +} + +/* Плохо: произвольный breakpoint вместо custom media. */ +.sidebar { + @media (min-width: 992px) { + display: block; + } +} +``` + +## CSS-переменные + +- Цвета (`--color-*`), отступы (`--space-*`), скругления (`--radius-*`) определяются в `src/shared/styles/variables.css` через `:root`. +- Файл переменных подключается через `src/shared/styles/global.css`, который импортируется один раз в `src/app/layout.tsx`. +- Не дублировать магические значения в компонентах. + +**Хорошо** +```css +/* src/shared/styles/variables.css */ +:root { + --color-primary: #3b82f6; + --color-bg: #ffffff; + --color-bg-hover: #f5f5f5; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --radius-1: 4px; + --radius-2: 8px; +} +``` + +```css +/* компонент */ +.card { + padding: var(--space-3); + border-radius: var(--radius-2); + background-color: var(--color-bg); +} +``` + +**Плохо** +```css +/* Плохо: магические значения вместо переменных. */ +.card { + padding: 12px; + border-radius: 8px; + background-color: #ffffff; +} +``` + +## Custom Media + +- Breakpoints определяются через Custom Media Queries в `src/shared/styles/media.css`. +- Custom media подключаются глобально через конфиг PostCSS (плагин `postcss-custom-media`) — не импортировать в файлы стилей. + +```css +/* src/shared/styles/media.css */ +@custom-media --sm (min-width: 36em); +@custom-media --md (min-width: 62em); +@custom-media --lg (min-width: 82em); +``` + +## Импорт стилей + +- Стили компонента импортируются только внутри своего компонента. +- Запрещено импортировать стили одного компонента в другой. +- Custom media не импортируются в файлы стилей — они подключаются глобально через конфиг PostCSS. + +## Форматирование + +- Пустая строка между селекторами верхнего уровня. +- Пустая строка перед каждым вложенным блоком (медиа, псевдокласс, модификатор). + +**Хорошо** +```css +.userBar { + display: none; + color: var(--color-text); + + @media (--md) { + display: flex; + } +} + +.userBarButton { + background-color: var(--color-bg); + + &:hover { + background-color: var(--color-bg-hover); + } + + &._active { + background-color: var(--color-primary); + } +} +``` + +**Плохо** +```css +/* Плохо: нет пустых строк между селекторами и вложенными блоками. */ +.userBar { + display: none; + color: var(--color-text); + @media (--md) { + display: flex; + } +} +.userBarButton { + background-color: var(--color-bg); + &:hover { + background-color: var(--color-bg-hover); + } + &._active { + background-color: var(--color-primary); + } +} +``` + +## Единицы измерения + +- `px` — основная единица измерения. +- Остальные (`em`, `rem`, `%`, `vh`/`vw`) — допускаются по необходимости дизайна. + +## Порядок CSS-свойств + +В стилях рекомендуется придерживаться логического порядка свойств: + +1. Позиционирование (`position`, `top`, `left`, `z-index`). +2. Блочная модель (`display`, `width`, `height`, `margin`, `padding`). +3. Оформление (`background`, `border`, `box-shadow`, `border-radius`). +4. Текст (`font`, `color`, `text-align`, `line-height`). +5. Прочее (`transition`, `animation`, `opacity`, `cursor`). + +## Комментарии + +- Желательно не писать комментарии в CSS. +- Исключение — нетривиальные хаки и обходные решения, к которым стоит оставить пояснение. diff --git a/canons/style-guide/applied/svg-sprites/svg-sprites-intro.md b/canons/style-guide/applied/svg-sprites/svg-sprites-intro.md new file mode 100644 index 0000000..68c2797 --- /dev/null +++ b/canons/style-guide/applied/svg-sprites/svg-sprites-intro.md @@ -0,0 +1,31 @@ +--- +title: SVG-спрайты +description: "Что такое SVG-спрайты и какие проблемы они решают." +--- + +# SVG-спрайты + +Что такое SVG-спрайты и какие проблемы они решают. + +## Проблема + +Иконки в проекте — это десятки и сотни SVG-файлов, которые нужно как-то доставлять в интерфейс. Подход «один `` на иконку» или инлайн SVG в каждом компоненте приводят к трём проблемам: + +- **Дублирование.** Инлайн SVG в нескольких компонентах — один и тот же код размазан по проекту. Изменение иконки требует правок в десяти местах. +- **Размер бандла.** Каждый инлайн SVG — полный XML-код, который попадает в JS-бандл. Сотня иконок × средний размер SVG = сотни килобайт, которые браузер парсит как JavaScript, а не как статику. +- **Нет управления цветом.** Инлайн SVG жёстко закрашивает иконку. Сменить цвет по состоянию (`:hover`, `._disabled`) — значит дублировать SVG или городить `currentColor`-хаки в каждом компоненте. + +## Решение + +SVG-спрайты — это единый файл-контейнер, в который собираются все иконки проекта. В коде используется один React-компонент ``, а браузер загружает спрайт как статику — один раз, с кешированием. + +Что дают SVG-спрайты: + +- **Один источник.** Каждая иконка — один SVG-файл в `src/shared/sprites/`. Обновил файл — иконка обновилась везде. +- **Лёгкий бандл.** Спрайт отдаётся как статический файл из `public/`, не попадает в JavaScript. Типы имён иконок генерируются автоматически — автодополнение работает без ручных описаний. +- **Цвет через CSS.** При сборке цвета в SVG заменяются на CSS-переменные. Цвет иконки меняется через `color` родителя или через переменные `--icon-color-N` — как любой другой стиль. + +## Состав раздела + +- [Настройка](/docs/applied/svg-sprites/svg-sprites-setup) — подключение пакета, конфигурация, первая генерация. +- [Использование](/docs/applied/svg-sprites/svg-sprites-usage) — добавление иконок, компонент ``, управление цветом. diff --git a/canons/style-guide/applied/svg-sprites/svg-sprites-setup.md b/canons/style-guide/applied/svg-sprites/svg-sprites-setup.md new file mode 100644 index 0000000..3d44c82 --- /dev/null +++ b/canons/style-guide/applied/svg-sprites/svg-sprites-setup.md @@ -0,0 +1,132 @@ +--- +title: Настройка SVG-спрайтов +description: Подключение SVG-спрайтов в новом проекте. +keywords: [svg-sprites, установка, настройка, config, пакет, "@gromlab/svg-sprites", svg-sprites.config.ts] +--- + +# Настройка SVG-спрайтов +Подключение SVG-спрайтов в новом проекте. + +## Установка + +1. Установить пакет: + + ```bash + npm install @gromlab/svg-sprites + ``` + +2. Создать `svg-sprites.config.ts` в корне проекта (см. [Стандартный конфиг](#стандартныи-конфиг)). + +3. Создать папку входа для SVG-файлов в слое `shared`: + + ```bash + mkdir -p src/shared/sprites/icons + ``` + + Источники спрайтов живут в `src/shared/sprites//` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим. + +4. Добавить скрипты в `package.json`: + + ```json + { + "scripts": { + "sprite": "svg-sprites", + "predev": "svg-sprites", + "prebuild": "svg-sprites" + } + } + ``` + + Хуки `predev` и `prebuild` гарантируют, что спрайты и типы всегда актуальны перед запуском и сборкой. + +5. Добавить сгенерированные артефакты в `.gitignore`: + + ```text + # Сгенерированные спрайты и React-компонент + /public/sprites/ + /src/ui/svg-sprite/ + ``` + + 6. Выполнить первую генерацию: + + ```bash + npm run sprite + ``` + +7. Подключить спрайт в layout. Глобальный спрайт (иконки) подключается через `` в корневом layout — браузер загрузит файл заранее и закеширует: + + ```tsx + // src/app/layout.tsx + import 'shared/styles/global.css' + + import type { Metadata } from 'next' + + export const metadata: Metadata = { + title: 'App', + description: '', + } + + export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + {children} + + ) + } + ``` + + Локальные спрайты (если есть) подключаются аналогично в layout конкретной страницы или маршрута. + +## Стандартный конфиг + +Файл `svg-sprites.config.ts` в корне проекта. Это канон — отклонения только по явной причине. + +```ts +// svg-sprites.config.ts +import { defineConfig } from '@gromlab/svg-sprites' + +export default defineConfig({ + output: 'public/sprites', + publicPath: '/sprites', + react: 'src/ui/svg-sprite', + sprites: [ + { name: 'icons', input: 'src/shared/sprites/icons' }, + ], +}) +``` + +### Фиксированные значения + +| Опция | Значение | Почему так | +|-------|----------|------------| +| `output` | `public/sprites` | Единая папка статики Next.js | +| `publicPath` | `/sprites` | URL-путь без `public/` (Next.js раздаёт `public/` как `/`) | +| `react` | `src/ui/svg-sprite` | Слой `ui/` из архитектуры проекта (→ [Архитектура](/docs/basics/architecture/)) | +| `sprites[0].name` | `icons` | Основной спрайт всегда называется `icons` | + +### Трансформации + +Все значения по умолчанию оставлять включёнными: + +```ts +transform: { + removeSize: true, + replaceColors: true, + addTransition: true, +} +``` + +Явно прописывать блок `transform` не нужно — пакет применяет эти значения по умолчанию. + +Отключать `replaceColors` — только для отдельного спрайта с фиксированной палитрой (например, брендовые логотипы). Делать это на уровне спрайта, не глобально. + +### Режим + +По умолчанию `mode: 'stack'` — не указывать явно. Переход на `symbol` требует обоснования: превью и примеры в пакете оптимизированы под `stack`. + +## Дальше + +- [Использование](/docs/applied/svg-sprites/svg-sprites-usage) — добавление иконок, компонент ``, управление цветом. diff --git a/canons/style-guide/applied/svg-sprites/svg-sprites-usage.md b/canons/style-guide/applied/svg-sprites/svg-sprites-usage.md new file mode 100644 index 0000000..d984642 --- /dev/null +++ b/canons/style-guide/applied/svg-sprites/svg-sprites-usage.md @@ -0,0 +1,56 @@ +--- +title: Использование SVG-спрайтов +description: Как добавлять и использовать SVG-иконки в коде. +keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превью, preview, цвет, color] +--- + +# Использование SVG-спрайтов + +Как добавлять и использовать SVG-иконки в коде. + +## Шаги + +1. **Положить SVG в папку спрайта:** + + ```text + src/shared/sprites/icons/new-icon.svg + ``` + +2. **Импортировать компонент.** Компонент `` генерируется пакетом вместе с типами имён иконок — автодополнение работает без ручных описаний: + + ```tsx + import { SvgSprite } from 'ui/svg-sprite' + + + ``` + +3. **Посмотреть и пощупать иконку — в превью.** Пакет генерирует HTML-превью рядом со спрайтом (`public/sprites/icons.preview.html`). Там виден набор иконок, имена и поведение цвета. + +## Управление цветом + +При сборке цвета в SVG заменяются на CSS-переменные `--icon-color-N`. Управление — через обычный CSS родителя. + +**Моно-иконка** наследует `color` родителя (`--icon-color-1` по умолчанию `currentColor`): + +```css +.button { + color: var(--color-primary); +} +``` + +**Точечное переопределение** — через переменную: + +```css +.icon-danger { + --icon-color-1: var(--color-danger); +} +``` + +**Мульти-иконка** — переменные задаются явно, порядок виден в превью: + +```css +.folder { + --icon-color-1: var(--color-folder-bg); + --icon-color-2: var(--color-folder-accent); +} +``` diff --git a/canons/style-guide/applied/templates/templates-create.md b/canons/style-guide/applied/templates/templates-create.md new file mode 100644 index 0000000..6b5f44e --- /dev/null +++ b/canons/style-guide/applied/templates/templates-create.md @@ -0,0 +1,97 @@ +--- +title: Создание шаблонов генерации +description: "Структура шаблонов, синтаксис переменных и примеры." +keywords: [шаблоны, templates, .templates, syntax, переменные, kebabCase, pascalCase, scaffold] +--- + + +::: v-pre + +# Создание шаблонов генерации + +Структура шаблонов, синтаксис переменных и примеры. + +## Структура шаблонов + +Все шаблоны лежат в `.templates/` в корне проекта. Каждая папка — отдельный шаблон. + +```text +.templates/ +├── component/ # шаблон компонента +│ └── {{name.kebabCase}}/ +│ ├── styles/ +│ │ └── {{name.kebabCase}}.module.css +│ ├── types/ +│ │ └── {{name.kebabCase}}-props.type.ts +│ ├── {{name.kebabCase}}.tsx +│ └── index.ts +└── store/ # шаблон Zustand стора + └── {{name.kebabCase}}/ + ├── {{name.kebabCase}}.store.ts + ├── {{name.kebabCase}}.type.ts + └── index.ts +``` + +## Обязательный шаблон компонента + +Перед созданием компонентов в проекте должен существовать шаблон `.templates/component`. + +Если шаблона нет, компонент не создаётся вручную. Сначала создаётся шаблон компонента, затем компонент генерируется через [VS Code или CLI](/docs/applied/templates/templates-usage). + +## Синтаксис шаблонов + +### Переменные + +Переменные работают в именах файлов/папок и внутри файлов: + +```text +{{variable}} +``` + +Переменные могут быть любыми. `name` — дефолтная, подставляется генератором автоматически. Если реализация требует дополнительных параметров — можно использовать произвольные наборы переменных. + +### Модификаторы + +Модификаторы меняют регистр и формат записи переменной: + +```text +{{name.pascalCase}} → MyButton +{{name.camelCase}} → myButton +{{name.kebabCase}} → my-button +{{name.snakeCase}} → my_button +{{name.screamingSnakeCase}} → MY_BUTTON +``` + +## Как создать новый шаблон + +1. Создать папку в `.templates/` с именем шаблона (например `hook`). +2. Внутри разместить файлы и папки, используя `{{name}}` и модификаторы в именах и содержимом. +3. Шаблон сразу доступен и в расширении VS Code, и в CLI. + +Пример — создание шаблона для хука: + +```text +.templates/ +└── hook/ + └── {{name.kebabCase}}/ + ├── {{name.kebabCase}}.hook.ts + └── index.ts +``` + +```ts +// .templates/hook/{{name.kebabCase}}.hook.ts +export const {{name.camelCase}} = () => { + +} +``` + +```ts +// .templates/hook/index.ts +export { {{name.camelCase}} } from './{{name.kebabCase}}.hook' +``` + +## Дальше + +- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI. + +::: diff --git a/canons/style-guide/applied/templates/templates-intro.md b/canons/style-guide/applied/templates/templates-intro.md new file mode 100644 index 0000000..f013907 --- /dev/null +++ b/canons/style-guide/applied/templates/templates-intro.md @@ -0,0 +1,32 @@ +--- +title: Шаблоны генерации +description: "Что такое шаблоны кодогенерации и какие проблемы они решают." +--- + +# Шаблоны генерации + +Что такое шаблоны кодогенерации и какие проблемы они решают. + +## Проблема + +Каждый новый модуль в проекте — компонент, стор, бизнес-модуль — требует однотипной структуры файлов и boilerplate-кода. Ручное создание приводит к трём проблемам: + +- **Расхождения.** Разные разработчики создают модули по-разному: забывают `index.ts`, называют типы не по канону, пропускают стили. +- **Время.** Создание одного компонента с типами, стилями и экспортом — 5–10 минут рутины. За спринт набегают часы. +- **Ошибки копипасты.** Копирование существующего модуля и переименование — источник опечаток и забытых ссылок. + +## Решение + +Шаблоны кодогенерации — это папки с файлами-заготовками в `.templates/`. Вместо ручного создания файлов разработчик вызывает генератор, указывает имя — и получает готовый модуль со всей структурой, именами и boilerplate, подставленными автоматически. + +Что дают шаблоны: + +- **Единообразие.** Все модули одного типа идентичны по структуре. Канон живёт в шаблоне, а не в памяти разработчика. +- **Скорость.** Генерация модуля — одна команда. Остальное время — на бизнес-логику. +- **Согласованность с архитектурой.** Шаблоны учитывают SLM: правильные слои, сегменты, экспорты. Отклонение от стайлгайда требует осознанного усилия, а не случайного упущения. + +## Состав раздела + +- [Настройка](/docs/applied/templates/templates-setup) — первичная установка: скачивание стандартного набора шаблонов в проект. +- [Создание шаблонов](/docs/applied/templates/templates-create) — структура файлов, синтаксис переменных, примеры. +- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI. diff --git a/canons/style-guide/applied/templates/templates-setup.md b/canons/style-guide/applied/templates/templates-setup.md new file mode 100644 index 0000000..f5afdc6 --- /dev/null +++ b/canons/style-guide/applied/templates/templates-setup.md @@ -0,0 +1,44 @@ +--- +title: Настройка шаблонов генерации +description: Первичная установка шаблонов кодогенерации в проект. +keywords: [шаблоны, templates, .templates, tiged, generator, генератор шаблонов, скачать шаблоны, scaffold] +--- + +# Настройка шаблонов генерации + +Первичная установка шаблонов кодогенерации в проект. + +## Установка + +1. Проверить, что `.templates/` отсутствует (или согласовать перезапись, если папка уже есть). + +2. Скачать папку из эталонного репозитория: + + ```bash + npx tiged git@gromlab.ru:templates/nextjs-template.git/.templates .templates + ``` + +3. Если `tiged` падает в default-режиме (HTTP-tarball у Gitea) — повторить с явным git-режимом: + + ```bash + npx tiged --mode=git git@gromlab.ru:templates/nextjs-template.git/.templates .templates + ``` + +4. Проверить генерацию: + + ```bash + npx @gromlab/create component test src/ui + ``` + + После проверки — удалить тестовый модуль. + +## Проверка установки + +- В корне проекта есть папка `.templates/`. +- Внутри `.templates/` присутствуют стандартные шаблоны (или согласованный кастомный набор). +- Пробная генерация через `npx @gromlab/create ...` отрабатывает без ошибок. + +## Дальше + +- [Создание шаблонов](/docs/applied/templates/templates-create) — структура файлов, синтаксис переменных, примеры. +- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI. diff --git a/canons/style-guide/applied/templates/templates-usage.md b/canons/style-guide/applied/templates/templates-usage.md new file mode 100644 index 0000000..80ff431 --- /dev/null +++ b/canons/style-guide/applied/templates/templates-usage.md @@ -0,0 +1,45 @@ +--- +title: Использование шаблонов генерации +description: Генерация файлов из шаблонов через VS Code плагин и CLI. +keywords: [шаблоны, templates, generate, VS Code, CLI, gromlab/create, npx, scaffold] +--- + +# Использование шаблонов генерации + +Генерация файлов из шаблонов через VS Code плагин и CLI. + +::: danger Ручное создание запрещено +Файлы, для которых есть шаблоны в `.templates/`, создаются только генератором. Ручное создание компонента, модуля, стора или другого шаблонного блока запрещено. + +Если нужного шаблона нет, сначала создайте шаблон в `.templates/`, затем сгенерируйте код на его основе. +::: + +## Через VS Code + +Template File Generator | gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) — расширение для генерации файлов и папок из шаблонов через интерфейс редактора. + +1. ПКМ на целевой папке в проводнике VS Code. +2. **Generate from template** → выбрать шаблон. +3. Ввести имя (например `button`) — расширение подставит его во все переменные `{{name}}`. + +Расширение устанавливается разово на машину разработчика, не через проект. + +## Через CLI + +[@gromlab/create](https://www.npmjs.com/package/@gromlab/create) — CLI для генерации из тех же шаблонов. Используется через npx, глобальная установка не требуется. + +```bash +npx @gromlab/create <шаблон> <имя> [путь] +``` + +Путь не обязателен — по умолчанию генерация происходит в текущую директорию. + +| Команда | Что создаёт | +|---|---| +| `npx @gromlab/create component button` | Компонент в текущей папке | +| `npx @gromlab/create module auth src/business` | Бизнес-модуль | +| `npx @gromlab/create widget header src/widgets` | Виджет | +| `npx @gromlab/create layout admin src/layouts` | Layout | +| `npx @gromlab/create store auth src/business/auth/stores` | Стор | + +CLI вызывается через `npx`, в `package.json` отдельно не добавляется. diff --git a/canons/style-guide/applied/vscode.md b/canons/style-guide/applied/vscode.md new file mode 100644 index 0000000..f844ead --- /dev/null +++ b/canons/style-guide/applied/vscode.md @@ -0,0 +1,88 @@ +--- +title: VS Code +description: Единые настройки редактора и расширений для команды. +--- + +# VS Code + +Единые настройки редактора и расширений для команды. + +## Структура `.vscode/` + +```text +.vscode/ +├── extensions.json # Рекомендуемые расширения +└── settings.json # Настройки редактора для проекта +``` + +Оба файла коммитятся в репозиторий. + +## Расширения + +Файл `.vscode/extensions.json` определяет список расширений, которые VS Code предложит установить при открытии проекта. + +```json +// .vscode/extensions.json +{ + "recommendations": [ + "biomejs.biome", + "MyTemplateGenerator.mytemplategenerator", + "csstools.postcss" + ] +} +``` + +| Расширение | Назначение | +|---|---| +| [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) | Линтинг и форматирование кода. Заменяет ESLint и Prettier | +| Template File Generator \| gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) | Генерация файлов и папок из шаблонов `.templates/` через контекстное меню | +| [PostCSS Language Support](https://marketplace.visualstudio.com/items?itemName=csstools.postcss) | Подсветка синтаксиса и автодополнение для PostCSS (`@custom-media`, `@nest` и др.) | + +### Зачем это нужно + +- Новый участник команды получает все нужные расширения одним кликом. +- Нет разночтений: все используют одинаковый форматтер и линтер. +- Расширения привязаны к проекту, а не к конкретному разработчику. + +## Настройки редактора + +Файл `.vscode/settings.json` переопределяет пользовательские настройки VS Code на уровне проекта. + +```json +// .vscode/settings.json +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "files.associations": { + "*.css": "postcss" + } +} +``` + +### Разбор настроек + +| Настройка | Значение | Что делает | +|---|---|---| +| `editor.defaultFormatter` | `biomejs.biome` | Biome используется как единственный форматтер для всех файлов | +| `editor.formatOnSave` | `true` | Код автоматически форматируется при каждом сохранении | +| `codeActionsOnSave.source.fixAll.biome` | `explicit` | Biome автоматически применяет безопасные исправления при сохранении | +| `codeActionsOnSave.source.organizeImports.biome` | `explicit` | Импорты сортируются и группируются автоматически при сохранении | +| `files.associations` | `"*.css": "postcss"` | Все CSS-файлы открываются с подсветкой PostCSS вместо стандартного CSS | + +### Зачем это нужно + +- **Единый стиль кода** -- форматирование происходит автоматически, невозможно закоммитить неформатированный код. +- **Автофикс при сохранении** -- распространённые ошибки линтинга исправляются без ручного вмешательства. +- **Сортировка импортов** -- импорты всегда в одном порядке, без конфликтов при мерже. +- **PostCSS-подсветка** -- кастомные at-правила (`@custom-media`, `@define-mixin`) подсвечиваются корректно, а не как ошибки. + +## Что не должно быть в `.vscode/` + +Не коммитятся файлы, специфичные для конкретного разработчика: + +- **Не коммитить**: отладочные конфигурации с локальными путями, персональные сниппеты, настройки тем оформления. +- **Коммитить**: только `extensions.json` и `settings.json` с общими для команды настройками. diff --git a/canons/style-guide/basics/architecture/index.md b/canons/style-guide/basics/architecture/index.md new file mode 100644 index 0000000..09a7173 --- /dev/null +++ b/canons/style-guide/basics/architecture/index.md @@ -0,0 +1,109 @@ +--- +title: SLM Design +description: Назначение архитектуры, ключевые принципы и карта разделов документации +--- + +# SLM Design +Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. + +## Разделы спецификации + +Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: + +- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. +- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. +- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. + +Рекомендуемый порядок чтения: обзор → слои → модули → сегменты. + +## Преимущества + +### Вертикальная организация домена + +Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы. + +### Dependency Injection без фреймворков + +Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий. + +### Разделение ответственности без перегрузки слоёв + +Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода. + +### Горизонтальная инкапсуляция + +Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. + +### Колокация по умолчанию + +Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. + +### Явное разделение каркаса и контента + +Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью. + +### Масштабирование через группировку + +При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции). + +## Происхождение + +SLM Design вырос на основе: + +- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей +- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое +- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию +- **Colocation Principle** — код живёт рядом с местом использования + +## Пример структуры проекта + +```text +src/ +├── app/ +│ +├── layouts/ +│ ├── main/ +│ └── dashboard/ +│ +├── screens/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── about/ +│ +├── widgets/ +│ ├── page-heading/ +│ ├── hero-section/ +│ └── promo-banner/ +│ +├── business/ +│ ├── auth/ +│ ├── catalog/ +│ ├── orders/ +│ └── chat/ +│ +├── infra/ +│ ├── theme/ +│ ├── i18n/ +│ ├── backend-api/ +│ └── logger/ +│ +├── ui/ +│ ├── button/ +│ ├── input/ +│ ├── modal/ +│ ├── toast/ +│ └── dropdown/ +│ +└── shared/ + ├── lib/ + ├── types/ + └── styles/ +``` + +## Принципы + +- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле. +- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. +- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API. +- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. diff --git a/canons/style-guide/basics/architecture/layers.md b/canons/style-guide/basics/architecture/layers.md new file mode 100644 index 0000000..92e7438 --- /dev/null +++ b/canons/style-guide/basics/architecture/layers.md @@ -0,0 +1,254 @@ +--- +title: Слои +description: Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя +--- + +# Слои + +Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. + +## Определение + +**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.** + +## Группы слоёв + +Слои делятся на три группы: + +| Группа | Слои | Описание | +|--------|------|----------| +| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы | +| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит | +| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги | + +## Направление зависимостей + +Любой импорт между модулями — только через публичный API. + +``` +app → [ layouts | screens ] → widgets → business → infra → ui → shared +``` + +- `layouts` и `screens` — параллельные слои, не импортируют друг друга +- Модули одного слоя в группе «Композиция» изолированы друг от друга +- Модули одного слоя `infra` и `ui` могут импортировать друг друга через публичный API +- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую +- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях + + +## Слой App + +Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen. + +В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации. + +### Требования + +- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация +- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов +- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует +- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы +- Никем не импортируется + +## Слой Layouts + +Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar). + +```text +src/layouts/ +├── main/ +├── dashboard/ +└── auth/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую + +## Слой Screens + +Контент конкретной страницы: собирает её из модулей нижних слоёв. + +```text +src/screens/ +├── home/ +├── products/ +├── product-detail/ +├── about/ +└── contacts/ +``` + +Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/screens/ +├── shop/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── cart/ +├── account/ +│ ├── profile/ +│ ├── settings/ +│ └── order-history/ +└── info/ + ├── about/ + ├── contacts/ + └── faq/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business` + +## Слой Widgets + +Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts. + +Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget. + +```text +src/widgets/ +├── page-heading/ +├── hero-section/ +├── onboarding-checklist/ +├── promo-banner/ +└── error-boundary/ +``` + +### Требования + +- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/` +- Используется в нескольких screens или layouts + +## Слой Business + +Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами. + +Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`. + +Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки. + +```text +src/business/ +├── auth/ +├── catalog/ +├── orders/ +├── checkout/ +└── chat/ +``` + +Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/business/ +├── commerce/ +│ ├── catalog/ +│ ├── cart/ +│ ├── orders/ +│ └── checkout/ +└── communication/ + ├── chat/ + └── notifications/ +``` + +### Требования + +- Один модуль = один бизнес-домен +- Циклические зависимости между доменами запрещены +- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты +- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую +- Доменные типы (`User`, `Product`) живут здесь, не в `shared/` + +## Слой infra + +Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль. + +Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. + +Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +```text +src/infra/ +├── theme/ +├── i18n/ +├── backend-api/ +├── maps-api/ +├── logger/ +├── feature-flags/ +└── realtime/ +``` + +### Требования + +- Один модуль = один техсервис +- Импортирует `infra/`, `ui/`, `shared/` + +## Слой UI + +UI-кит без бизнес-логики: button, carousel, toast, modal. + +Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`. + +Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. + +```text +src/ui/ +├── button/ +├── input/ +├── icon/ +├── carousel/ +├── modal/ +├── toast/ +├── dropdown/ +├── tabs/ +└── tooltip/ +``` + +Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах. + +```text +src/ui/ +├── primitives/ +│ ├── button/ +│ ├── input/ +│ ├── icon/ +│ └── badge/ +└── composites/ + ├── carousel/ + ├── modal/ + ├── dropdown/ + ├── tabs/ + └── tooltip/ +``` + +### Требования + +- Не содержит бизнес-логику +- Импортирует только `ui/` и `shared/` + +## Слой Shared + +Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене. + +Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует. + +Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь. + +```text +src/shared/ +├── lib/ +├── types/ +├── styles/ +└── sprites/ +``` + +### Требования + +- Не имеет runtime-состояния diff --git a/canons/style-guide/basics/architecture/modules.md b/canons/style-guide/basics/architecture/modules.md new file mode 100644 index 0000000..4950d7c --- /dev/null +++ b/canons/style-guide/basics/architecture/modules.md @@ -0,0 +1,289 @@ +--- +title: Модули +description: Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента +--- + +# Модули + +Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом. + +## Определение + +**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.** + +Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно. + +Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность. + +Главная граница модуля — не папка, а ответственность. + +## Компонент + +**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.** + +Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля. + +> Компонент отображает. Модуль организует. + +Компонент не может: + +- Импортировать код проекта за пределами родительского модуля. +- Владеть архитектурными зависимостями. +- Содержать любые компоненты. +- Содержать любые модули. +- Делать внешние запросы. +- Самостоятельно получать данные. +- Выбирать источник данных. +- Композировать данные. +- Вызывать сценарные хуки. +- Оркестрировать сценарий. +- Композировать модули. +- Решать, как устроен процесс. +- Содержать бизнес-логику. +- Содержать сценарную логику. + +Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль. + +```text +auth/ +├── ui/ +│ └── logout-button/ +│ ├── logout-button.tsx +│ ├── styles/ +│ │ └── logout-button.module.css +│ ├── types/ +│ │ └── logout-button-props.type.ts +│ └── index.ts +└── index.ts +``` + +## Что считается модулем + +Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу. + +Примеры модулей: + +- `screens/home/` — модуль страницы. +- `widgets/page-heading/` — модуль виджета. +- `business/auth/` — модуль бизнес-домена. +- `infra/theme/` — модуль инфраструктурного сервиса. +- `ui/button/` — модуль UI-kit сущности. +- `screens/home/parts/hero-section/` — вложенный модуль страницы. + +Не считаются модулями: + +- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты. +- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`. +- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента. + +## Типы модулей + +Тип модуля определяет обязательный корневой файл и стартовую структуру. + +### UI-модуль + +Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне: + +```text +header/ +├── header.tsx +└── index.ts +``` + +`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу. + +### Бизнес-модуль + +Бизнес-модуль — модуль, который строится вокруг публичного runtime API. + +Бизнес-модуль обязан иметь фабрику в корне: + +```text +auth/ +├── auth.factory.ts +├── index.ts +└── types/ +``` + +Фабрика возвращает публичный runtime API модуля. + +### Инфраструктурный модуль + +Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции. + +Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса. + +```text +theme/ +├── index.ts +├── config/ +├── hooks/ +├── styles/ +└── ui/ +``` + +```text +backend-api/ +├── backend-api.client.ts +├── config/ +├── types/ +└── index.ts +``` + +## Структура + +Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности. + +```text +{module-name}/ +├── {module-name}.factory.ts # фабрика (для business-модулей) +├── {module-name}.tsx # корневой файл модуля (опционален) +├── ui/ # компоненты модуля +├── parts/ # вложенные модули +├── hooks/ # хуки +├── stores/ # сторы состояния +├── services/ # внешние источники данных +├── mappers/ # трансформация данных между форматами +├── types/ # типы +├── styles/ # стили +├── lib/ # утилиты модуля +├── config/ # константы и конфигурация +└── index.ts # публичный API +``` + +Подробное описание сегментов — в разделе [Сегменты](./segments.md). + +## Публичный API + +Внешний код импортирует модуль только через публичный API. + +```ts +// Хорошо +import { customerFactory } from '@/business/customer' +import type { Customer } from '@/business/customer' +``` + +```ts +// Плохо +import { validateToken } from '@/business/auth/lib/tokens' +``` + +`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи. + +Внутренние сегменты модуля остаются деталями реализации. + +Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика. + +```ts +// business/customer/index.ts +export { customerFactory } from './customer.factory' + +export type { Customer } from './types/customer.type' +export type { CustomerApi } from './types/customer-api.type' +export type { CustomerDeps } from './types/customer-deps.type' +export type { CustomerFactory } from './types/customer-factory.type' +``` + +## Фабрика + +Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля. + +Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика. + +Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью. + +Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». + +### Структура business-модуля + +```text +business/customer/ +├── customer.factory.ts +├── index.ts +└── types/ + ├── customer.type.ts + ├── customer-api.type.ts + ├── customer-deps.type.ts + └── customer-factory.type.ts +``` + +### Типы + +```ts +// business/customer/types/customer-api.type.ts +export type CustomerApi = { + useCustomer: () => Customer + CustomerCard: (props: CustomerCardProps) => ReactNode +} +``` + +```ts +// business/order/types/order-deps.type.ts +export type OrderDeps = { + customer: Pick +} +``` + +```ts +// business/order/types/order-factory.type.ts +export type OrderFactory = (deps: OrderDeps) => OrderApi +``` + +### Фабрика без зависимостей + +```ts +// business/customer/customer.factory.ts +import type { CustomerFactory } from './types/customer-factory.type' + +export const customerFactory: CustomerFactory = () => { + return { + useCustomer, + CustomerCard, + } +} +``` + +### Фабрика с зависимостями + +```ts +// business/order/order.factory.ts +import type { OrderFactory } from './types/order-factory.type' + +export const orderFactory: OrderFactory = (deps) => { + return { + useOrder, + OrderCard, + } +} +``` + +### Композиция на уровне screen + +```tsx +// screens/home/home.screen.tsx +import { customerFactory } from '@/business/customer' +import { orderFactory } from '@/business/order' + +const customer = customerFactory() +const order = orderFactory({ customer }) + +const { useOrder, OrderCard } = order + +export const HomeScreen = () => { + const currentOrder = useOrder() + + return +} +``` + +## Жизненный цикл + +Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. + +- Нужен на одной странице → `screens/{name}/parts/` +- Появился в 2+ местах → поднимается по природе: + - абстрактный UI → `ui/` + - блок с данными/логикой → `widgets/` + - представление бизнес-домена → `business/{area}/parts/` + +Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/canons/style-guide/basics/architecture/segments.md b/canons/style-guide/basics/architecture/segments.md new file mode 100644 index 0000000..eeb1b52 --- /dev/null +++ b/canons/style-guide/basics/architecture/segments.md @@ -0,0 +1,181 @@ +--- +title: Сегменты +description: Сегменты внутри модуля (ui/, model/, lib/ и др.), назначение и правила размещения файлов +--- + +# Сегменты + +Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. + +## Определение + +**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** + +## Обзор + +| Сегмент | Содержимое | +|---------|------------| +| `ui/` | Презентационные компоненты родительского модуля | +| `parts/` | Вложенные модули со своими сегментами | +| `hooks/` | React-хуки | +| `stores/` | Сторы состояния | +| `services/` | Работа с внешними источниками данных | +| `mappers/` | Трансформация данных между форматами | +| `types/` | TypeScript-типы и интерфейсы | +| `styles/` | Стили | +| `lib/` | Утилиты и хелперы модуля | +| `config/` | Константы и конфигурация | + +## Сегмент ui/ + +Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля. + +Компонент в `ui/`: + +- Находится в собственной папке. +- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`. +- Не содержит любые компоненты. +- Не содержит любые модули. +- Не импортирует код проекта за пределами родительского модуля. +- Не делает внешние запросы. +- Не вызывает сценарные хуки. +- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. +- Не содержит бизнес-логику или сценарную логику. + +Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент). + +Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. + +```text +user/ +├── ui/ +│ ├── user-avatar/ +│ │ ├── user-avatar.tsx +│ │ ├── styles/ +│ │ │ └── user-avatar.module.css +│ │ ├── types/ +│ │ │ └── user-avatar-props.type.ts +│ │ └── index.ts +│ └── user-status/ +│ ├── user-status.tsx +│ └── index.ts +├── types/ +├── hooks/ +├── user.tsx +└── index.ts +``` + +Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`. + +## Сегмент parts/ + +Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются. + +```text +home/ +├── parts/ +│ ├── hero-section/ +│ │ ├── hero-section.tsx +│ │ ├── styles/ +│ │ ├── parts/ +│ │ │ └── top-banner/ +│ │ │ ├── top-banner.tsx +│ │ │ └── index.ts +│ │ └── index.ts +│ └── features-section/ +│ ├── features-section.tsx +│ ├── hooks/ +│ └── index.ts +├── home.screen.tsx +└── index.ts +``` + +Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности. + +Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке. + +Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше. + +## Сегмент hooks/ + +React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты. + +```text +hooks/ +├── use-auth.hook.ts +├── use-session.hook.ts +└── use-permissions.hook.ts +``` + +## Сегмент stores/ + +Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.). + +```text +stores/ +├── auth.store.ts +└── session.store.ts +``` + +## Сегмент services/ + +Работа с внешними источниками данных: API-вызовы, запросы, подписки. + +```text +services/ +├── auth.service.ts +└── token.service.ts +``` + +## Сегмент mappers/ + +Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel. + +```text +mappers/ +├── map-user.ts +├── map-product.ts +└── map-order-to-dto.ts +``` + +## Сегмент types/ + +TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов. + +```text +types/ +├── user.type.ts +└── session.type.ts +``` + +## Сегмент styles/ + +Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.). + +```text +styles/ +├── auth.module.css +└── login-form.module.css +``` + +## Сегмент lib/ + +Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов. + +```text +lib/ +├── validate-email.ts +└── format-phone.ts +``` + +Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`. + +## Сегмент config/ + +Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения. + +```text +config/ +├── routes.ts +└── constants.ts +``` diff --git a/canons/style-guide/basics/code-style.md b/canons/style-guide/basics/code-style.md new file mode 100644 index 0000000..e47e625 --- /dev/null +++ b/canons/style-guide/basics/code-style.md @@ -0,0 +1,153 @@ +--- +title: Стиль кода +description: Как оформляется код в проекте. +--- + +# Стиль кода + +Как оформляется код в проекте. + +## Отступы + +- 2 пробела (не табы). + +## Длина строк + +- Ориентироваться на 100 символов, но превышение допустимо, если строка читается легко. +- Переносить выражение на новые строки, когда строка становится плохо читаемой. +- Не переносить строку внутри строковых литералов без необходимости. + +**Хорошо** +```ts +const config = createRequestConfig( + endpoint, + { + headers: { + 'X-Request-Id': requestId, + 'X-User-Id': userId, + }, + params: { + page, + pageSize, + sort: 'createdAt', + }, + }, + timeoutMs, +); +``` + +**Плохо** +```ts +// Плохо: длинная строка с вложенными структурами плохо читается. +const config = createRequestConfig(endpoint, { headers: { 'X-Request-Id': requestId, 'X-User-Id': userId }, params: { page, pageSize, sort: 'createdAt' } }, timeoutMs); +``` + +## Кавычки + +- В JavaScript/TypeScript использовать одинарные кавычки. +- В JSX/TSX для атрибутов использовать двойные кавычки. +- Шаблонные строки использовать только при интерполяции или многострочном тексте. + +**Хорошо** +```ts +const label = 'Сохранить'; +const title = `Привет, ${name}`; +``` + +```tsx + +``` + +**Плохо** +```ts +// Плохо: двойные кавычки в TS и конкатенация вместо шаблонной строки. +const label = "Сохранить"; +const title = 'Привет, ' + name; +``` + +```tsx +// Плохо: одинарные кавычки в JSX-атрибутах. + +``` + +## Точки с запятой и запятые + +- Допускаются упущения точки с запятой, если код остаётся читаемым и однозначным. +- В многострочных массивах, объектах и параметрах функции запятая в конце допускается, но не обязательна. + +## Импорты + +- В именованных импортах использовать пробелы внутри фигурных скобок. +- Типы импортировать через `import type`. +- `default` экспорт избегать, использовать именованные. `default` импорт допустим (например, стили CSS Modules, сторонние библиотеки). +- Избегать импорта всего модуля через `*`. + +**Хорошо** +```ts +import { MyComponent } from 'MyComponent'; +import type { User } from '../model/types'; +import styles from './styles/button.module.css'; +``` + +**Плохо** +```ts +// Плохо: отсутствие пробелов в именованном импорте. +import type {User} from '../model/types'; +// Плохо: default экспорт. +export default MyComponent; +``` + +## Ранние возвраты (early return) + +- Использовать ранние возвраты для упрощения чтения. +- Избегать `else` после `return`. + +**Хорошо** +```ts +const getName = (user?: { name: string }) => { + if (!user) { + return 'Гость'; + } + + return user.name; +}; +``` + +**Плохо** +```ts +// Плохо: лишний else после return усложняет чтение. +const getName = (user?: { name: string }) => { + if (user) { + return user.name; + } else { + return 'Гость'; + } +}; +``` + +## Форматирование объектов и массивов + +- В многострочных объектах каждое свойство на новой строке. +- В многострочных массивах каждый элемент на новой строке. +- Объекты и массивы можно писать в одну строку, если длина строки не превышает 100 символов. +- В однострочных объектах и массивах использовать пробелы после запятых. + +**Хорошо** +```ts +const roles = ['admin', 'editor', 'viewer']; +const options = { id: 1, name: 'User' }; + +const config = { + url: '/api/users', + method: 'GET', + params: { page: 1, pageSize: 20 }, +}; +``` + +**Плохо** +```ts +// Плохо: нет пробелов после запятых и объект слишком длинный для одной строки. +const roles = ['admin','editor','viewer']; +const options = { id: 1,name: 'User' }; +const config = { url: '/api/users', method: 'GET', params: { page: 1, pageSize: 20 } }; +``` diff --git a/canons/style-guide/basics/documentation.md b/canons/style-guide/basics/documentation.md new file mode 100644 index 0000000..81abcf1 --- /dev/null +++ b/canons/style-guide/basics/documentation.md @@ -0,0 +1,134 @@ +--- +title: Документирование +description: Что и как документировать в коде. +--- + +# Документирование + +Что и как документировать в коде. + +## Общие правила + +- Документировать публичные функции, компоненты, типы, интерфейсы и enum. +- Не документировать очевидное — если название говорит само за себя, комментарий не нужен. +- Не документировать параметры, возвращаемые значения и типы пропсов — они видны из сигнатуры. +- Описание через пользу и назначение, а не через внутреннюю реализацию. +- Описание завершается точкой. + +## Функции + +Для документирования функций используется шаблон. Описание механики опционально — +добавляется когда логика нетривиальна. + +**Шаблон** +```ts +/** + * <Что делает функция в 1 строке>. + * + * <Опционально: описание сложной механики или важных нюансов>. + */ +``` + +**Хорошо** +```ts +/** + * Форматирует цену с символом валюты. + */ +export const formatPrice = (value: number): string => { ... } + +/** + * Рекурсивно собирает дерево категорий из плоского списка. + * + * Группирует элементы по parentId, начиная с корневых (parentId = null). + * Категории без родителя попадают в корень дерева. + */ +export const buildCategoryTree = (categories: Category[]): CategoryTree[] => { ... } +``` + +**Плохо** +```ts +// Плохо: дублирует сигнатуру. +/** + * @param value - число + * @returns строка с ценой + */ +``` + +## Компоненты + +Компонент описывает своё **назначение** и **сценарии применения** — это помогает понять, когда и где его использовать, без необходимости читать реализацию. + +**Шаблон** +```ts +/** + * <Назначение компонента в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + * - <сценарий 3> + */ +``` + +**Хорошо** +```tsx +/** + * Контейнер с адаптивной максимальной шириной. + * + * Используется для: + * - обёртки контента страниц с ограничением ширины + * - центрирования блоков в лейауте + */ +export const Container = (props: ContainerProps) => { ... } +``` + +**Плохо** +```tsx +// Плохо: описывает реализацию, а не назначение. +/** + * Рендерит div с className и htmlAttr. + */ + +// Плохо: нет описания вообще. +export const Container = (props: ContainerProps) => { ... } +``` + +## Типы, интерфейсы, enum + +Документируются назначение сущности и каждое её поле. + +**Хорошо** +```ts +/** + * Фильтры списка задач. + */ +export enum TodoFilter { + /** Все задачи. */ + ALL = 'all', + /** Только активные. */ + ACTIVE = 'active', + /** Только завершённые. */ + COMPLETED = 'completed', +} + +/** + * Задача пользователя. + */ +export interface TodoItem { + /** Уникальный идентификатор задачи. */ + id: string; + /** Текст задачи. */ + text: string; + /** Статус выполнения. */ + completed: boolean; +} +``` + +**Плохо** +```ts +// Плохо: описывает очевидное. +export interface TodoItem { + /** id — это id */ + id: string; +} +``` diff --git a/canons/style-guide/basics/naming.md b/canons/style-guide/basics/naming.md new file mode 100644 index 0000000..096dffb --- /dev/null +++ b/canons/style-guide/basics/naming.md @@ -0,0 +1,146 @@ +--- +title: Именование +description: Как называть переменные, файлы и прочие сущности в коде. +--- + +# Именование + +Как называть переменные, файлы и прочие сущности в коде. + +## Базовые правила + +| Что | Рекомендуется | +| ---------------- | ---------------------- | +| Папки | `kebab-case` | +| Файлы | `kebab-case` | +| Переменные | `camelCase` | +| Константы | `SCREAMING_SNAKE_CASE` | +| Классы | `PascalCase` | +| React-компоненты | `PascalCase` | +| Хуки | `useSomething` | +| CSS классы | `camelCase` | +| Ключи enum | `SCREAMING_SNAKE_CASE` | + + +## Именование файлов + +Суффикс обозначает роль или тип файла. Пишется в единственном числе. +Формат: `name..ts`. + +**Хуки** +- `use-name.hook.ts` — файл хука, функция именуется `useName` + +**Логика** +- `.store.ts` — стор +- `.service.ts` — сервис + +**Корневые компоненты слоёв** +- `.screen.tsx` — корневой компонент screen-модуля: `screens/profile/profile.screen.tsx`, компонент `ProfileScreen` +- `.layout.tsx` — корневой компонент layout-модуля: `layouts/main/main.layout.tsx`, компонент `MainLayout` + +Обычные и вложенные модули не получают суффикс слоя: `ui/button/button.tsx`, `screens/profile/parts/activity-feed/activity-feed.tsx`. + +**Типы и контракты** +- `.type.ts` — типы и интерфейсы +- `.interface.ts` — интерфейсы +- `.enum.ts` — enum +- `.dto.ts` — внешние DTO +- `.schema.ts` — схемы валидации +- `.constant.ts` — константы +- `.config.ts` — конфигурация + +**Утилиты** +- `.util.ts` — утилиты +- `.helper.ts` — вспомогательные функции +- `.lib.ts` — библиотечный код + +**Тесты** +- `.test.ts` — тесты +- `.mock.ts` — моки + +**Хорошо** +```text +business/ +└── auth-by-email/ + ├── ui/ + │ └── login-form.tsx + ├── hooks/ + │ └── use-auth.hook.ts + ├── stores/ + │ └── auth.store.ts + ├── types/ + │ └── auth.type.ts + ├── auth-by-email.tsx + └── index.ts +``` + +**Плохо** +```text +business/ +└── authByEmail/ + ├── LoginForm.tsx + ├── useAuth.ts + ├── authStore.ts + └── index.ts +``` + +## Булевы значения + +- Использовать префиксы `is`, `has`, `can`, `should`. + +**Хорошо** +```ts +const isReady = true; +const hasAccess = false; +const canSubmit = true; +const shouldRedirect = false; +``` + +**Плохо** +```ts +// Плохо: неясное булево значение без префикса. +const ready = true; +const access = false; +const submit = true; +``` + +## События и обработчики + +- Обработчики начинать с `handle`. +- События и колбэки начинать с `on`. + +**Хорошо** +```ts +const handleSubmit = () => { ... }; +const onSubmit = () => { ... }; +``` + +**Плохо** +```ts +// Плохо: неочевидное назначение имени. +const submitClick = () => { ... }; +``` + +## Коллекции + +- Для массивов использовать имена во множественном числе. +- Для словарей/мап — использовать суффиксы `ById`, `Map`, `Dict`. + +**Хорошо** +```ts +const users = []; +const usersById = {} as Record; +const userIds = ['u1', 'u2']; +const ordersMap = new Map(); +const featureFlagsDict = { beta: true, legacy: false } as Record; +``` + +**Плохо** +```ts +// Плохо: имя не отражает, что это коллекция. +const user = []; +// Плохо: словарь назван как массив. +const usersMap = []; +// Плохо: по имени непонятно, что это словарь. +const users = {} as Record; +``` diff --git a/canons/style-guide/basics/tech-stack.md b/canons/style-guide/basics/tech-stack.md new file mode 100644 index 0000000..a42d452 --- /dev/null +++ b/canons/style-guide/basics/tech-stack.md @@ -0,0 +1,42 @@ +--- +title: Технологии и библиотеки +description: Какие библиотеки и инструменты используются в проекте. +--- + +# Технологии и библиотеки + +Какие библиотеки и инструменты используются в проекте. + +## Что используем + +### Стек +- `React` / `TypeScript` — основной стек для UI и приложения. +- `Next.js` — для продуктовых сайтов. + +### Архитектура +- `SLM Design` — собственная модульная архитектура проекта. Подробнее в разделе [Архитектура](/docs/basics/architecture/). + +### UI компоненты +- `Mantine UI` — базовые UI-компоненты. + +### Работа с данными (API) +- `@gromlab/api-codegen` — генерация API‑клиентов и типов. +- `SWR` — получение, кеширование, ревалидация, дедубликация. +- `SWR (useSWRSubscription)` — сокеты, реалтайм подписки. + +### Store +- `Zustand` — глобальное состояние. + +### Локализация +- `i18next (i18n)` — локализация всех пользовательских текстов. + +### Тестирование +- `Vitest` — тестирование. + +### Стили +- `PostCSS Modules` — изоляция стилей. +- `Mobile First` — подход к адаптивной верстке. +- `clsx` — конкатенация CSS‑классов. + +### Генерация +- `@gromlab/create` — шаблонизатор для создания слоёв и других файлов из шаблонов. diff --git a/canons/style-guide/basics/typing.md b/canons/style-guide/basics/typing.md new file mode 100644 index 0000000..840765f --- /dev/null +++ b/canons/style-guide/basics/typing.md @@ -0,0 +1,62 @@ +--- +title: Типизация +description: Как типизируется код в проекте. +--- + +# Типизация + +Как типизируется код в проекте. + +## Общие правила + +- Указывать типы для параметров компонентов и параметров функций. +- Предпочитать `type` для описания сущностей и `interface` для расширяемых контрактов. +- Избегать `any` и `unknown` без необходимости. +- Не использовать `ts-ignore`, кроме крайних случаев с явным комментарием причины. + +## React-компоненты + +- Пропсы компонента типизировать через отдельный `Props`. +- Возвращаемый тип компонента не указывать: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата. + +## Функции + +- Для публичных функций указывать возвращаемый тип. +- Не полагаться на неявный вывод для важных API. + +**Хорошо** +```ts +export const formatPrice = (value: number): string => { + return `${value} ₽`; +}; +``` + +**Плохо** +```ts +// Плохо: нет явного возвращаемого типа. +export const formatPrice = (value: number) => { + return `${value} ₽`; +}; +``` + +## Работа с any/unknown + +- `any` использовать только для временных заглушек. +- `unknown` сужать через проверки перед использованием. + +**Хорошо** +```ts +const parse = (value: unknown): string => { + if (typeof value === 'string') { + return value; + } + + return ''; +}; +``` + +**Плохо** +```ts +// Плохо: any отключает проверку типов. +const parse = (value: any) => value; +``` diff --git a/canons/style-guide/index.md b/canons/style-guide/index.md new file mode 100644 index 0000000..8cec273 --- /dev/null +++ b/canons/style-guide/index.md @@ -0,0 +1,79 @@ +--- +title: NextJS Style Guide +description: Стандарты разработки фронтенд-приложений на Next.js и TypeScript. +--- + +# NextJS Style Guide + +Стандарты разработки фронтенд-приложений на Next.js и TypeScript. + +## Использование + +**Для AI-агентов:** + +- [llms.txt](/llms.txt) — Карта разделов, оглавление со ссылками на разделы. +- [llms-full.txt](/llms-full.txt) — Вся документация одним файлом. + +**Для проекта:** + +- [nextjs-style-guide.zip](/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта. + +## Структура документации + +### Подсказки + +[Подсказки](/docs/workflow) — короткие ответы на типовые вопросы и решения для спорных ситуаций. + +### Базовые правила + +**Каким должен быть код** — стандарты, не привязанные к конкретной технологии. + +| Раздел | Отвечает на вопрос | +|--------|-------------------| +| Технологии и библиотеки | Какой стек используем? | +| Именование | Как называть файлы, переменные, компоненты, хуки? | +| SLM Design | Что такое SLM и зачем она нужна? | +| Архитектура: Слои | Какие слои есть и как между ними устроены зависимости? | +| Архитектура: Модули | Что такое модуль и как он устроен? | +| Архитектура: Сегменты | Какие сегменты есть внутри модуля? | +| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? | +| Документирование | Как писать JSDoc: что документировать, а что нет? | +| Типизация | Как типизировать: type vs interface, any/unknown? | + +### Настройка + +**Как сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры. + +| Раздел | Отвечает на вопрос | +|--------|-------------------| +| Создание проекта из шаблона | Как начать проект из готового шаблона? | +| Создание проекта вручную | Как поднять проект с нуля без шаблона? | +| Чистая установка Next.js | Как поставить голый Next.js под дальнейшую сборку? | +| Алиасы импортов | Как настроить алиасы импортов? | +| Biome | Как настроить линтер и форматтер? | +| PostCSS | Какие плагины PostCSS нужны и как их настроить? | +| Стили | Как подключить базовые стили и токены? | +| SVG-спрайты | Как подключить генерацию SVG-спрайтов? | +| Шаблоны генерации | Как подключить шаблоны для кодогенерации? | +| VS Code | Как настроить редактор для проекта? | + +### Использование + +**Как это устроено и как этим пользоваться** — структура, примеры и правила для конкретных областей. + +| Раздел | Отвечает на вопрос | +|--------|-------------------| +| Структура проекта | Как организованы папки и файлы по SLM? | +| Компоненты | Как устроен компонент: файлы, пропсы, clsx? | +| Файлы роутинга | Как описывать layout, page, loading, error, not-found? | +| REST-клиент | Как настроить клиент внешнего REST API? | +| Получение данных | Как выбрать способ получения данных под рендер страницы? | +| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? | +| Стили | Как писать CSS: вложенность, медиа, токены? | +| SVG-спрайты | Как использовать SVG-спрайты в коде? | +| Изображения | _(не заполнен)_ | +| Видео | _(не заполнен)_ | +| Stores | _(не заполнен)_ | +| Хуки | _(не заполнен)_ | +| Шрифты | _(не заполнен)_ | +| Локализация | _(не заполнен)_ | diff --git a/canons/style-guide/slm-design/VERSION b/canons/style-guide/slm-design/VERSION new file mode 100644 index 0000000..d0b7f5d --- /dev/null +++ b/canons/style-guide/slm-design/VERSION @@ -0,0 +1,2 @@ +v0.1.5 +2026-05-11T18:29:26.821Z diff --git a/canons/style-guide/slm-design/architecture/index.md b/canons/style-guide/slm-design/architecture/index.md new file mode 100644 index 0000000..51a7f16 --- /dev/null +++ b/canons/style-guide/slm-design/architecture/index.md @@ -0,0 +1,114 @@ +--- +title: SLM Design +description: "Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили." +--- + +# SLM Design +Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. + +## Разделы спецификации + +Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: + +- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. +- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. +- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. +- [Монорепозитории](./monorepo.md) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business. + +Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории. + +## Преимущества + +### Вертикальная организация домена + +Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы. + +### Dependency Injection без фреймворков + +Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий. + +### Разделение ответственности без перегрузки слоёв + +Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода. + +### Горизонтальная инкапсуляция + +Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. + +### Колокация по умолчанию + +Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. + +### Явное разделение каркаса и контента + +Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью. + +### Масштабирование через группировку + +При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции). + +### Адаптация к монорепозиториям + +SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы. + +## Происхождение + +SLM Design вырос на основе: + +- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей +- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое +- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию +- **Colocation Principle** — код живёт рядом с местом использования + +## Пример структуры проекта + +```text +src/ +├── app/ +│ +├── layouts/ +│ ├── main/ +│ └── dashboard/ +│ +├── screens/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── about/ +│ +├── widgets/ +│ ├── page-heading/ +│ ├── hero-section/ +│ └── promo-banner/ +│ +├── business/ +│ ├── auth/ +│ ├── catalog/ +│ ├── orders/ +│ └── chat/ +│ +├── infra/ +│ ├── theme/ +│ ├── i18n/ +│ ├── backend-api/ +│ └── logger/ +│ +├── ui/ +│ ├── button/ +│ ├── input/ +│ ├── modal/ +│ ├── toast/ +│ └── dropdown/ +│ +└── shared/ + ├── lib/ + ├── types/ + └── styles/ +``` + +## Принципы + +- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле. +- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. +- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API. +- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. diff --git a/canons/style-guide/slm-design/architecture/layers.md b/canons/style-guide/slm-design/architecture/layers.md new file mode 100644 index 0000000..335c978 --- /dev/null +++ b/canons/style-guide/slm-design/architecture/layers.md @@ -0,0 +1,254 @@ +--- +title: Слои +description: "Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом." +--- + +# Слои + +Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. + +## Определение + +**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.** + +## Группы слоёв + +Слои делятся на три группы: + +| Группа | Слои | Описание | +|--------|------|----------| +| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы | +| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит | +| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги | + +## Направление зависимостей + +Любой импорт между модулями — только через публичный API. + +``` +app → [ layouts | screens ] → widgets → business → infra → ui → shared +``` + +- `layouts` и `screens` — параллельные слои, не импортируют друг друга +- Модули одного слоя в группе «Композиция» изолированы друг от друга +- Модули одного слоя `infra` и `ui` могут импортировать друг друга через публичный API +- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую +- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях + + +## Слой App + +Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen. + +В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации. + +### Требования + +- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация +- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов +- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует +- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы +- Никем не импортируется + +## Слой Layouts + +Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar). + +```text +src/layouts/ +├── main/ +├── dashboard/ +└── auth/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую + +## Слой Screens + +Контент конкретной страницы: собирает её из модулей нижних слоёв. + +```text +src/screens/ +├── home/ +├── products/ +├── product-detail/ +├── about/ +└── contacts/ +``` + +Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/screens/ +├── shop/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── cart/ +├── account/ +│ ├── profile/ +│ ├── settings/ +│ └── order-history/ +└── info/ + ├── about/ + ├── contacts/ + └── faq/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business` + +## Слой Widgets + +Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts. + +Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget. + +```text +src/widgets/ +├── page-heading/ +├── hero-section/ +├── onboarding-checklist/ +├── promo-banner/ +└── error-boundary/ +``` + +### Требования + +- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/` +- Используется в нескольких screens или layouts + +## Слой Business + +Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами. + +Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`. + +Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки. + +```text +src/business/ +├── auth/ +├── catalog/ +├── orders/ +├── checkout/ +└── chat/ +``` + +Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/business/ +├── commerce/ +│ ├── catalog/ +│ ├── cart/ +│ ├── orders/ +│ └── checkout/ +└── communication/ + ├── chat/ + └── notifications/ +``` + +### Требования + +- Один модуль = один бизнес-домен +- Циклические зависимости между доменами запрещены +- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты +- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую +- Доменные типы (`User`, `Product`) живут здесь, не в `shared/` + +## Слой infra + +Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль. + +Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. + +Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +```text +src/infra/ +├── theme/ +├── i18n/ +├── backend-api/ +├── maps-api/ +├── logger/ +├── feature-flags/ +└── realtime/ +``` + +### Требования + +- Один модуль = один техсервис +- Импортирует `infra/`, `ui/`, `shared/` + +## Слой UI + +UI-кит без бизнес-логики: button, carousel, toast, modal. + +Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`. + +Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. + +```text +src/ui/ +├── button/ +├── input/ +├── icon/ +├── carousel/ +├── modal/ +├── toast/ +├── dropdown/ +├── tabs/ +└── tooltip/ +``` + +Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах. + +```text +src/ui/ +├── primitives/ +│ ├── button/ +│ ├── input/ +│ ├── icon/ +│ └── badge/ +└── composites/ + ├── carousel/ + ├── modal/ + ├── dropdown/ + ├── tabs/ + └── tooltip/ +``` + +### Требования + +- Не содержит бизнес-логику +- Импортирует только `ui/` и `shared/` + +## Слой Shared + +Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене. + +Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует. + +Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь. + +```text +src/shared/ +├── lib/ +├── types/ +├── styles/ +└── sprites/ +``` + +### Требования + +- Не имеет runtime-состояния diff --git a/canons/style-guide/slm-design/architecture/modules.md b/canons/style-guide/slm-design/architecture/modules.md new file mode 100644 index 0000000..2b318c4 --- /dev/null +++ b/canons/style-guide/slm-design/architecture/modules.md @@ -0,0 +1,213 @@ +--- +title: Модули +description: "Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом." +--- + +# Модули + +Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом. + +## Определение + +**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.** + +Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно. + +Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность. + +Главная граница модуля — не папка, а ответственность. + +## Компонент + +**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.** + +Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля. + +> Компонент отображает. Модуль организует. + +Компонент не может: + +- Импортировать код проекта за пределами родительского модуля. +- Владеть архитектурными зависимостями. +- Содержать любые компоненты. +- Содержать любые модули. +- Делать внешние запросы. +- Самостоятельно получать данные. +- Выбирать источник данных. +- Композировать данные. +- Вызывать сценарные хуки. +- Оркестрировать сценарий. +- Композировать модули. +- Решать, как устроен процесс. +- Содержать бизнес-логику. +- Содержать сценарную логику. + +Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль. + +```text +auth/ +├── ui/ +│ └── logout-button/ +│ ├── logout-button.tsx +│ ├── styles/ +│ │ └── logout-button.module.css +│ ├── types/ +│ │ └── logout-button-props.type.ts +│ └── index.ts +└── index.ts +``` + +## Что считается модулем + +Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу. + +Примеры модулей: + +- `screens/home/` — модуль страницы. +- `widgets/page-heading/` — модуль виджета. +- `business/auth/` — модуль бизнес-домена. +- `infra/theme/` — модуль инфраструктурного сервиса. +- `ui/button/` — модуль UI-kit сущности. +- `screens/home/parts/hero-section/` — вложенный модуль страницы. + +Не считаются модулями: + +- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты. +- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`. +- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента. + +## Типы модулей + +Тип модуля определяет обязательный корневой файл и стартовую структуру. + +### UI-модуль + +Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне: + +```text +header/ +├── header.tsx +└── index.ts +``` + +`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу. + +### Бизнес-модуль + +Бизнес-модуль — модуль, который строится вокруг публичного runtime API. + +Бизнес-модуль обязан иметь фабрику в корне: + +```text +auth/ +├── auth.factory.ts +├── index.ts +└── types/ +``` + +Фабрика возвращает публичный runtime API модуля. + +### Инфраструктурный модуль + +Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции. + +Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса. + +```text +theme/ +├── index.ts +├── config/ +├── hooks/ +├── styles/ +└── ui/ +``` + +```text +backend-api/ +├── backend-api.client.ts +├── config/ +├── types/ +└── index.ts +``` + +## Структура + +Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности. + +```text +{module-name}/ +├── {module-name}.factory.ts # фабрика (для business-модулей) +├── {module-name}.tsx # корневой файл модуля (опционален) +├── ui/ # компоненты модуля +├── parts/ # вложенные модули +├── hooks/ # хуки +├── stores/ # сторы состояния +├── services/ # внешние источники данных +├── mappers/ # трансформация данных между форматами +├── types/ # типы +├── styles/ # стили +├── lib/ # утилиты модуля +├── config/ # константы и конфигурация +└── index.ts # публичный API +``` + +Подробное описание сегментов — в разделе [Сегменты](./segments.md). + +## Публичный API + +Внешний код импортирует модуль только через публичный API. + +```ts +// Хорошо +import { customerFactory } from '@/business/customer' +import type { Customer } from '@/business/customer' +``` + +```ts +// Плохо +import { validateToken } from '@/business/auth/lib/tokens' +``` + +`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи. + +Внутренние сегменты модуля остаются деталями реализации. + +Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика. + +```ts +// business/customer/index.ts +export { customerFactory } from './customer.factory' + +export type { Customer } from './types/customer.type' +export type { CustomerApi } from './types/customer-api.type' +export type { CustomerDeps } from './types/customer-deps.type' +export type { CustomerFactory } from './types/customer-factory.type' +``` + +## Фабрика + +Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля. + +Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика. + +Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью. + +Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». + +### Примеры + +Пример реализации фабрики в React см. в [Создание фабрики](../examples/react/factory.md). + +Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](../examples/react/factory-composition.md). + +## Жизненный цикл + +Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. + +- Нужен на одной странице → `screens/{name}/parts/` +- Появился в 2+ местах → поднимается по природе: + - абстрактный UI → `ui/` + - блок с данными/логикой → `widgets/` + - представление бизнес-домена → `business/{area}/parts/` + +Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/canons/style-guide/slm-design/architecture/monorepo.md b/canons/style-guide/slm-design/architecture/monorepo.md new file mode 100644 index 0000000..eb1f5d2 --- /dev/null +++ b/canons/style-guide/slm-design/architecture/monorepo.md @@ -0,0 +1,235 @@ +--- +title: Монорепозитории +description: "Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов." +--- + +# Монорепозитории + +Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов. + +## Определение + +**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.** + +## Базовая структура + +Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`. + +```text +repo/ +├── apps/ +│ ├── web/ +│ │ └── src/ +│ │ ├── app/ +│ │ ├── layouts/ +│ │ ├── screens/ +│ │ ├── widgets/ +│ │ ├── business/ +│ │ ├── infra/ +│ │ ├── ui/ +│ │ └── shared/ +│ └── admin/ +│ └── src/ +│ └── ... +└── packages/ + ├── ui/ + │ ├── button/ # самостоятельный пакет UI-модуля + │ ├── input/ # самостоятельный пакет UI-модуля + │ └── modal/ # самостоятельный пакет UI-модуля + ├── infra/ + │ ├── theme/ # самостоятельный пакет infra-модуля + │ ├── backend-api/ # самостоятельный пакет infra-модуля + │ └── logger/ # самостоятельный пакет infra-модуля + └── shared/ # единый shared-пакет + ├── package.json + └── src/ + ├── lib/ # переиспользуемые утилиты + ├── helpers/ # переиспользуемые helpers + └── index.ts +``` + +`apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои. + +## Группировка frontend-пакетов + +Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`. + +```text +packages/ui/* # пакеты UI-модулей +packages/infra/* # пакеты infra-модулей +packages/shared # единый shared-пакет +``` + +Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM. + +## Пакет и модуль + +Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации. + +В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению. + +```text +packages/ui/button/ +packages/ui/modal/ +packages/infra/theme/ +packages/infra/backend-api/ +packages/shared/ +``` + +## Что остаётся в приложении + +Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения. + +```text +apps/web/src/app/ +apps/web/src/layouts/ +apps/web/src/screens/ +apps/web/src/widgets/ +apps/web/src/business/ +``` + +`app`, `layouts` и `screens` привязаны к роутингу, каркасу и страницам конкретного приложения. `widgets` не выносятся в пакеты, потому что это слой композиции интерфейса приложения. + +`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой. + +## Что можно выносить + +В пакеты выносится только код из `ui`, `infra` и `shared`, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория. + +| Группа | Что выносить | Пример | +|--------|--------------|--------| +| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` | +| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` | +| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` | + +Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения. + +## UI-пакеты + +В `packages/ui/*` размещаются переиспользуемые UI-модули. + +```text +packages/ui/button/ +├── package.json +└── src/ + ├── button.tsx + ├── styles/ + ├── types/ + └── index.ts +``` + +UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц. + +## Infra-пакеты + +В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули. + +```text +packages/infra/backend-api/ +├── package.json +└── src/ + ├── clients/ + ├── config/ + ├── types/ + └── index.ts +``` + +Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок. + +## Shared-пакет + +`packages/shared` является единым пакетом. + +```text +packages/shared/ +├── package.json +└── src/ + ├── lib/ + ├── helpers/ + └── index.ts +``` + +В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте. + +Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся. + +## Имена пакетов и импорты + +Путь импорта задаётся `name` в `package.json`, а не расположением директории. + +```json +{ + "name": "@repo/theme" +} +``` + +```text +packages/infra/theme/package.json +``` + +```ts +import { ThemeProvider } from '@repo/theme' +``` + +Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены. + +```ts +// Хорошо +import { Button } from '@repo/button' + +// Плохо +import { Button } from '@repo/button/src/button' +``` + +## Зависимости + +На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений. + +```text +apps → packages +packages -/→ apps +``` + +Внутри приложения продолжает действовать обычное направление зависимостей SLM. + +```text +app → [ layouts | screens ] → widgets → business → infra → ui → shared +``` + +Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях. + +## Когда не выносить + +Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API. + +Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям. + +```text +# Плохо +apps/web/src/screens/home/parts/promo-section/ +packages/ui/promo-section/ +``` + +Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным `parts/`-модулем. + +## Конфигурационные пакеты + +Конфигурационные пакеты не относятся к SLM-архитектуре. + +Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`. + +## Правила + +- SLM применяется внутри каждого `apps/{app}/src`. +- Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`. +- Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями. +- В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. +- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. +- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers. +- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях. +- `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты. +- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`. +- Пакеты не импортируют приложения. +- Межпакетные импорты идут только через публичный API. +- Deep imports внутрь пакетов запрещены. +- Локальная колокация важнее преждевременного выноса в `packages/*`. diff --git a/canons/style-guide/slm-design/architecture/segments.md b/canons/style-guide/slm-design/architecture/segments.md new file mode 100644 index 0000000..ba11b77 --- /dev/null +++ b/canons/style-guide/slm-design/architecture/segments.md @@ -0,0 +1,181 @@ +--- +title: Сегменты +description: "Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит." +--- + +# Сегменты + +Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. + +## Определение + +**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** + +## Обзор + +| Сегмент | Содержимое | +|---------|------------| +| `ui/` | Презентационные компоненты родительского модуля | +| `parts/` | Вложенные модули со своими сегментами | +| `hooks/` | React-хуки | +| `stores/` | Сторы состояния | +| `services/` | Работа с внешними источниками данных | +| `mappers/` | Трансформация данных между форматами | +| `types/` | TypeScript-типы и интерфейсы | +| `styles/` | Стили | +| `lib/` | Утилиты и хелперы модуля | +| `config/` | Константы и конфигурация | + +## Сегмент ui/ + +Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля. + +Компонент в `ui/`: + +- Находится в собственной папке. +- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`. +- Не содержит любые компоненты. +- Не содержит любые модули. +- Не импортирует код проекта за пределами родительского модуля. +- Не делает внешние запросы. +- Не вызывает сценарные хуки. +- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. +- Не содержит бизнес-логику или сценарную логику. + +Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент). + +Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. + +```text +user/ +├── ui/ +│ ├── user-avatar/ +│ │ ├── user-avatar.tsx +│ │ ├── styles/ +│ │ │ └── user-avatar.module.css +│ │ ├── types/ +│ │ │ └── user-avatar-props.type.ts +│ │ └── index.ts +│ └── user-status/ +│ ├── user-status.tsx +│ └── index.ts +├── types/ +├── hooks/ +├── user.tsx +└── index.ts +``` + +Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`. + +## Сегмент parts/ + +Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются. + +```text +home/ +├── parts/ +│ ├── hero-section/ +│ │ ├── hero-section.tsx +│ │ ├── styles/ +│ │ ├── parts/ +│ │ │ └── top-banner/ +│ │ │ ├── top-banner.tsx +│ │ │ └── index.ts +│ │ └── index.ts +│ └── features-section/ +│ ├── features-section.tsx +│ ├── hooks/ +│ └── index.ts +├── home.screen.tsx +└── index.ts +``` + +Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности. + +Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке. + +Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше. + +## Сегмент hooks/ + +React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты. + +```text +hooks/ +├── use-auth.hook.ts +├── use-session.hook.ts +└── use-permissions.hook.ts +``` + +## Сегмент stores/ + +Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.). + +```text +stores/ +├── auth.store.ts +└── session.store.ts +``` + +## Сегмент services/ + +Работа с внешними источниками данных: API-вызовы, запросы, подписки. + +```text +services/ +├── auth.service.ts +└── token.service.ts +``` + +## Сегмент mappers/ + +Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel. + +```text +mappers/ +├── map-user.ts +├── map-product.ts +└── map-order-to-dto.ts +``` + +## Сегмент types/ + +TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов. + +```text +types/ +├── user.type.ts +└── session.type.ts +``` + +## Сегмент styles/ + +Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.). + +```text +styles/ +├── auth.module.css +└── login-form.module.css +``` + +## Сегмент lib/ + +Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов. + +```text +lib/ +├── validate-email.ts +└── format-phone.ts +``` + +Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`. + +## Сегмент config/ + +Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения. + +```text +config/ +├── routes.ts +└── constants.ts +``` diff --git a/canons/style-guide/slm-design/examples/react/composition-provider.md b/canons/style-guide/slm-design/examples/react/composition-provider.md new file mode 100644 index 0000000..4f5593b --- /dev/null +++ b/canons/style-guide/slm-design/examples/react/composition-provider.md @@ -0,0 +1,249 @@ +--- +title: Композиция через Provider +description: "Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя." +--- + +# Композиция через Provider + +Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя. + +## Идея + +Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга. + +## Принципы + +1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах. +2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей. +3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`. +4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen. +5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает. +6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики. + +## Структура модуля + +```text +screens/main/ +├── main.screen.tsx +├── providers/ +│ └── main-composition.provider.tsx +├── hooks/ +│ └── use-main-composition.hook.ts +├── types/ +│ └── main-composition.type.ts +├── parts/ +│ └── featured-products/ +│ ├── featured-products.tsx +│ └── index.ts +└── index.ts +``` + +Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются. + +## Распределение по сегментам + +| Файл | Сегмент | Назначение | +|------|---------|------------| +| `main-composition.type.ts` | `types/` | TypeScript-тип композиции | +| `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент | +| `use-main-composition.hook.ts` | `hooks/` | React-хук доступа | +| `main.screen.tsx` | корень | Корневой компонент screen-модуля | +| `featured-products/` | `parts/` | Вложенный модуль со своим публичным API | + +## Тип композиции + +Файл: `screens/main/types/main-composition.type.ts`. + +Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen. + +```ts +import type { CatalogApi } from '@/business/catalog' +import type { CartApi } from '@/business/cart' + +export type MainComposition = { + catalog: CatalogApi + cart: CartApi +} +``` + +## Context и Provider + +Файл: `screens/main/providers/main-composition.provider.tsx`. + +Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве. + +Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой. + +```tsx +import { createContext, type ReactNode } from 'react' +import type { MainComposition } from '../types/main-composition.type' + +export const MainCompositionContext = createContext(null) + +type Props = { + value: MainComposition + children: ReactNode +} + +export const MainCompositionProvider = ({ value, children }: Props) => ( + + {children} + +) +``` + +## Хук доступа + +Файл: `screens/main/hooks/use-main-composition.hook.ts`. + +Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`. + +Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева. + +```ts +import { useContext } from 'react' +import { MainCompositionContext } from '../providers/main-composition.provider' + +export const useMainComposition = () => { + const ctx = useContext(MainCompositionContext) + if (!ctx) { + throw new Error('useMainComposition must be used within MainCompositionProvider') + } + return ctx +} +``` + +## Сборка графа в роутере + +Файл: `app/router.tsx`. + +Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики. + +Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента. + +```tsx +import { MainScreen, MainCompositionProvider } from '@/screens/main' +import { catalogFactory } from '@/business/catalog' +import { cartFactory } from '@/business/cart' +import { authFactory } from '@/business/auth' + +const auth = authFactory() +const catalog = catalogFactory() +const cart = cartFactory({ auth }) + +const MainRoute = () => ( + + + +) +``` + +## Корневой компонент screen + +Файл: `screens/main/main.screen.tsx`. + +Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку. + +```tsx +import { useMainComposition } from './hooks/use-main-composition.hook' +import { FeaturedProducts } from './parts/featured-products' + +export const MainScreen = () => { + const { catalog } = useMainComposition() + const { useCategories, CategoryList } = catalog + const categories = useCategories() + + return ( +
+ + +
+ ) +} +``` + +## Вложенный part + +Файл: `screens/main/parts/featured-products/featured-products.tsx`. + +Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props. + +Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую. + +```tsx +import { useMainComposition } from '../../hooks/use-main-composition.hook' + +export const FeaturedProducts = () => { + const { catalog, cart } = useMainComposition() + const { useFeatured, ProductCard } = catalog + const { addItem } = cart + const products = useFeatured() + + return ( +
+ {products.map((product) => ( + addItem(product.id)} + /> + ))} +
+ ) +} +``` + +Файл: `screens/main/parts/featured-products/index.ts`. + +```ts +export { FeaturedProducts } from './featured-products' +``` + +## Публичный API screen-модуля + +Файл: `screens/main/index.ts`. + +Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации. + +```ts +export { MainScreen } from './main.screen' +export { MainCompositionProvider } from './providers/main-composition.provider' +``` + +## Почему тип композиции не экспортируется + +Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen. + +Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen. + +```ts +import type { MainComposition } from '@/screens/main' +``` + +Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля. + +## Почему хук не экспортируется + +Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`. + +## Почему Provider экспортируется + +Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева. + +## Стабильность value + +Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`. + +Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию. + +## Расширение на другие screen-модули + +Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов. + +```text +screens/checkout/providers/checkout-composition.provider.tsx +screens/checkout/hooks/use-checkout-composition.hook.ts +screens/checkout/types/checkout-composition.type.ts +``` + +Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context. diff --git a/canons/style-guide/slm-design/examples/react/factory-composition.md b/canons/style-guide/slm-design/examples/react/factory-composition.md new file mode 100644 index 0000000..460321a --- /dev/null +++ b/canons/style-guide/slm-design/examples/react/factory-composition.md @@ -0,0 +1,52 @@ +--- +title: Композиция фабрик +description: "Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов." +--- + +# Композиция фабрик + +Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов. + +## Идея + +Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик. + +## Структура screen-модуля + +```text +screens/home/ +├── home.screen.tsx +└── index.ts +``` + +## Сборка фабрик + +Файл: `screens/home/home.screen.tsx`. + +```tsx +import { customerFactory } from '@/business/customer' +import { orderFactory } from '@/business/order' + +const customer = customerFactory() +const order = orderFactory({ customer }) + +const { useOrder, OrderCard } = order + +export const HomeScreen = () => { + const currentOrder = useOrder() + + return +} +``` + +`customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи. + +## Публичный API screen-модуля + +Файл: `screens/home/index.ts`. + +```ts +export { HomeScreen } from './home.screen' +``` + +Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля. diff --git a/canons/style-guide/slm-design/examples/react/factory.md b/canons/style-guide/slm-design/examples/react/factory.md new file mode 100644 index 0000000..e9d8abb --- /dev/null +++ b/canons/style-guide/slm-design/examples/react/factory.md @@ -0,0 +1,114 @@ +--- +title: Создание фабрики +description: "Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API." +--- + +# Создание фабрики + +Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API. + +## Структура business-модуля + +Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`. + +```text +business/customer/ +├── customer.factory.ts +├── hooks/ +├── types/ +│ ├── customer.type.ts +│ ├── customer-api.type.ts +│ └── customer-factory.type.ts +├── ui/ +└── index.ts +``` + +## Тип публичного API + +Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы. + +```ts +// business/customer/types/customer-api.type.ts +import type { ReactNode } from 'react' +import type { Customer } from './customer.type' + +export type CustomerCardProps = { + customer: Customer +} + +export type CustomerApi = { + useCustomer: () => Customer | null + CustomerCard: (props: CustomerCardProps) => ReactNode +} +``` + +```ts +// business/customer/types/customer-factory.type.ts +import type { CustomerApi } from './customer-api.type' + +export type CustomerFactory = () => CustomerApi +``` + +## Фабрика без зависимостей + +Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов. + +```ts +// business/customer/customer.factory.ts +import { useCustomer } from './hooks/use-customer.hook' +import { CustomerCard } from './ui/customer-card' +import type { CustomerFactory } from './types/customer-factory.type' + +export const customerFactory: CustomerFactory = () => { + return { + useCustomer, + CustomerCard, + } +} +``` + +```ts +// business/customer/index.ts +export { customerFactory } from './customer.factory' + +export type { Customer } from './types/customer.type' +export type { CustomerApi } from './types/customer-api.type' +export type { CustomerFactory } from './types/customer-factory.type' +``` + +## Фабрика с зависимостями + +Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API. + +```ts +// business/order/types/order-deps.type.ts +import type { CustomerApi } from '@/business/customer' + +export type OrderDeps = { + customer: Pick +} +``` + +```ts +// business/order/types/order-factory.type.ts +import type { OrderApi } from './order-api.type' +import type { OrderDeps } from './order-deps.type' + +export type OrderFactory = (deps: OrderDeps) => OrderApi +``` + +```ts +// business/order/order.factory.ts +import { createUseOrder } from './hooks/use-order.hook' +import { OrderCard } from './ui/order-card' +import type { OrderFactory } from './types/order-factory.type' + +export const orderFactory: OrderFactory = (deps) => { + const useOrder = createUseOrder(deps) + + return { + useOrder, + OrderCard, + } +} +``` diff --git a/canons/style-guide/workflow.md b/canons/style-guide/workflow.md new file mode 100644 index 0000000..c060231 --- /dev/null +++ b/canons/style-guide/workflow.md @@ -0,0 +1,8 @@ +--- +title: Подсказки +description: Короткие ответы на типовые вопросы и решения для спорных ситуаций. +--- + +# Подсказки + +Короткие ответы на типовые вопросы и решения для спорных ситуаций. diff --git a/docs/slm-design/.vitepress/cache/deps_temp_f6246001/chunk-BKAC4ZFU.js b/docs/slm-design/.vitepress/cache/deps_temp_f6246001/chunk-BKAC4ZFU.js new file mode 100644 index 0000000..9bf83be --- /dev/null +++ b/docs/slm-design/.vitepress/cache/deps_temp_f6246001/chunk-BKAC4ZFU.js @@ -0,0 +1,13035 @@ +// node_modules/@vue/shared/dist/shared.esm-bundler.js +function makeMap(str) { + const map2 = /* @__PURE__ */ Object.create(null); + for (const key of str.split(",")) map2[key] = 1; + return (val) => val in map2; +} +var EMPTY_OBJ = true ? Object.freeze({}) : {}; +var EMPTY_ARR = true ? Object.freeze([]) : []; +var NOOP = () => { +}; +var NO = () => false; +var isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter +(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97); +var isModelListener = (key) => key.startsWith("onUpdate:"); +var extend = Object.assign; +var remove = (arr, el) => { + const i = arr.indexOf(el); + if (i > -1) { + arr.splice(i, 1); + } +}; +var hasOwnProperty = Object.prototype.hasOwnProperty; +var hasOwn = (val, key) => hasOwnProperty.call(val, key); +var isArray = Array.isArray; +var isMap = (val) => toTypeString(val) === "[object Map]"; +var isSet = (val) => toTypeString(val) === "[object Set]"; +var isDate = (val) => toTypeString(val) === "[object Date]"; +var isRegExp = (val) => toTypeString(val) === "[object RegExp]"; +var isFunction = (val) => typeof val === "function"; +var isString = (val) => typeof val === "string"; +var isSymbol = (val) => typeof val === "symbol"; +var isObject = (val) => val !== null && typeof val === "object"; +var isPromise = (val) => { + return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch); +}; +var objectToString = Object.prototype.toString; +var toTypeString = (value) => objectToString.call(value); +var toRawType = (value) => { + return toTypeString(value).slice(8, -1); +}; +var isPlainObject = (val) => toTypeString(val) === "[object Object]"; +var isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key; +var isReservedProp = makeMap( + // the leading comma is intentional so empty string "" is also included + ",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted" +); +var isBuiltInDirective = makeMap( + "bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo" +); +var cacheStringFunction = (fn) => { + const cache = /* @__PURE__ */ Object.create(null); + return (str) => { + const hit = cache[str]; + return hit || (cache[str] = fn(str)); + }; +}; +var camelizeRE = /-\w/g; +var camelize = cacheStringFunction( + (str) => { + return str.replace(camelizeRE, (c) => c.slice(1).toUpperCase()); + } +); +var hyphenateRE = /\B([A-Z])/g; +var hyphenate = cacheStringFunction( + (str) => str.replace(hyphenateRE, "-$1").toLowerCase() +); +var capitalize = cacheStringFunction((str) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}); +var toHandlerKey = cacheStringFunction( + (str) => { + const s = str ? `on${capitalize(str)}` : ``; + return s; + } +); +var hasChanged = (value, oldValue) => !Object.is(value, oldValue); +var invokeArrayFns = (fns, ...arg) => { + for (let i = 0; i < fns.length; i++) { + fns[i](...arg); + } +}; +var def = (obj, key, value, writable = false) => { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + writable, + value + }); +}; +var looseToNumber = (val) => { + const n = parseFloat(val); + return isNaN(n) ? val : n; +}; +var toNumber = (val) => { + const n = isString(val) ? Number(val) : NaN; + return isNaN(n) ? val : n; +}; +var _globalThis; +var getGlobalThis = () => { + return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {}); +}; +var GLOBALS_ALLOWED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol"; +var isGloballyAllowed = makeMap(GLOBALS_ALLOWED); +function normalizeStyle(value) { + if (isArray(value)) { + const res = {}; + for (let i = 0; i < value.length; i++) { + const item = value[i]; + const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item); + if (normalized) { + for (const key in normalized) { + res[key] = normalized[key]; + } + } + } + return res; + } else if (isString(value) || isObject(value)) { + return value; + } +} +var listDelimiterRE = /;(?![^(]*\))/g; +var propertyDelimiterRE = /:([^]+)/; +var styleCommentRE = /\/\*[^]*?\*\//g; +function parseStringStyle(cssText) { + const ret = {}; + cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => { + if (item) { + const tmp = item.split(propertyDelimiterRE); + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); + } + }); + return ret; +} +function stringifyStyle(styles) { + if (!styles) return ""; + if (isString(styles)) return styles; + let ret = ""; + for (const key in styles) { + const value = styles[key]; + if (isString(value) || typeof value === "number") { + const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key); + ret += `${normalizedKey}:${value};`; + } + } + return ret; +} +function normalizeClass(value) { + let res = ""; + if (isString(value)) { + res = value; + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const normalized = normalizeClass(value[i]); + if (normalized) { + res += normalized + " "; + } + } + } else if (isObject(value)) { + for (const name in value) { + if (value[name]) { + res += name + " "; + } + } + } + return res.trim(); +} +function normalizeProps(props) { + if (!props) return null; + let { class: klass, style } = props; + if (klass && !isString(klass)) { + props.class = normalizeClass(klass); + } + if (style) { + props.style = normalizeStyle(style); + } + return props; +} +var HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot"; +var SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view"; +var MATH_TAGS = "annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics"; +var VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr"; +var isHTMLTag = makeMap(HTML_TAGS); +var isSVGTag = makeMap(SVG_TAGS); +var isMathMLTag = makeMap(MATH_TAGS); +var isVoidTag = makeMap(VOID_TAGS); +var specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; +var isSpecialBooleanAttr = makeMap(specialBooleanAttrs); +var isBooleanAttr = makeMap( + specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected` +); +function includeBooleanAttr(value) { + return !!value || value === ""; +} +var isKnownHtmlAttr = makeMap( + `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap` +); +var isKnownSvgAttr = makeMap( + `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan` +); +var isKnownMathMLAttr = makeMap( + `accent,accentunder,actiontype,align,alignmentscope,altimg,altimg-height,altimg-valign,altimg-width,alttext,bevelled,close,columnsalign,columnlines,columnspan,denomalign,depth,dir,display,displaystyle,encoding,equalcolumns,equalrows,fence,fontstyle,fontweight,form,frame,framespacing,groupalign,height,href,id,indentalign,indentalignfirst,indentalignlast,indentshift,indentshiftfirst,indentshiftlast,indextype,justify,largetop,largeop,lquote,lspace,mathbackground,mathcolor,mathsize,mathvariant,maxsize,minlabelspacing,mode,other,overflow,position,rowalign,rowlines,rowspan,rquote,rspace,scriptlevel,scriptminsize,scriptsizemultiplier,selection,separator,separators,shift,side,src,stackalign,stretchy,subscriptshift,superscriptshift,symmetric,voffset,width,widths,xlink:href,xlink:show,xlink:type,xmlns` +); +function isRenderableAttrValue(value) { + if (value == null) { + return false; + } + const type = typeof value; + return type === "string" || type === "number" || type === "boolean"; +} +var cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g; +function getEscapedCssVarName(key, doubleEscape) { + return key.replace( + cssVarNameEscapeSymbolsRE, + (s) => doubleEscape ? s === '"' ? '\\\\\\"' : `\\\\${s}` : `\\${s}` + ); +} +function looseCompareArrays(a, b) { + if (a.length !== b.length) return false; + let equal = true; + for (let i = 0; equal && i < a.length; i++) { + equal = looseEqual(a[i], b[i]); + } + return equal; +} +function looseEqual(a, b) { + if (a === b) return true; + let aValidType = isDate(a); + let bValidType = isDate(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? a.getTime() === b.getTime() : false; + } + aValidType = isSymbol(a); + bValidType = isSymbol(b); + if (aValidType || bValidType) { + return a === b; + } + aValidType = isArray(a); + bValidType = isArray(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? looseCompareArrays(a, b) : false; + } + aValidType = isObject(a); + bValidType = isObject(b); + if (aValidType || bValidType) { + if (!aValidType || !bValidType) { + return false; + } + const aKeysCount = Object.keys(a).length; + const bKeysCount = Object.keys(b).length; + if (aKeysCount !== bKeysCount) { + return false; + } + for (const key in a) { + const aHasKey = a.hasOwnProperty(key); + const bHasKey = b.hasOwnProperty(key); + if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) { + return false; + } + } + } + return String(a) === String(b); +} +function looseIndexOf(arr, val) { + return arr.findIndex((item) => looseEqual(item, val)); +} +var isRef = (val) => { + return !!(val && val["__v_isRef"] === true); +}; +var toDisplayString = (val) => { + return isString(val) ? val : val == null ? "" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? isRef(val) ? toDisplayString(val.value) : JSON.stringify(val, replacer, 2) : String(val); +}; +var replacer = (_key, val) => { + if (isRef(val)) { + return replacer(_key, val.value); + } else if (isMap(val)) { + return { + [`Map(${val.size})`]: [...val.entries()].reduce( + (entries, [key, val2], i) => { + entries[stringifySymbol(key, i) + " =>"] = val2; + return entries; + }, + {} + ) + }; + } else if (isSet(val)) { + return { + [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v)) + }; + } else if (isSymbol(val)) { + return stringifySymbol(val); + } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) { + return String(val); + } + return val; +}; +var stringifySymbol = (v, i = "") => { + var _a; + return ( + // Symbol.description in es2019+ so we need to cast here to pass + // the lib: es2016 check + isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v + ); +}; +function normalizeCssVarValue(value) { + if (value == null) { + return "initial"; + } + if (typeof value === "string") { + return value === "" ? " " : value; + } + if (typeof value !== "number" || !Number.isFinite(value)) { + if (true) { + console.warn( + "[Vue warn] Invalid value used for CSS binding. Expected a string or a finite number but received:", + value + ); + } + } + return String(value); +} + +// node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js +function warn(msg, ...args) { + console.warn(`[Vue warn] ${msg}`, ...args); +} +var activeEffectScope; +var EffectScope = class { + // TODO isolatedDeclarations "__v_skip" + constructor(detached = false) { + this.detached = detached; + this._active = true; + this._on = 0; + this.effects = []; + this.cleanups = []; + this._isPaused = false; + this._warnOnRun = true; + this.__v_skip = true; + if (!detached && activeEffectScope) { + if (activeEffectScope.active) { + this.parent = activeEffectScope; + this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( + this + ) - 1; + } else { + this._active = false; + this._warnOnRun = false; + } + } + } + get active() { + return this._active; + } + pause() { + if (this._active) { + this._isPaused = true; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].pause(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].pause(); + } + } + } + /** + * Resumes the effect scope, including all child scopes and effects. + */ + resume() { + if (this._active) { + if (this._isPaused) { + this._isPaused = false; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].resume(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].resume(); + } + } + } + } + run(fn) { + if (this._active) { + const currentEffectScope = activeEffectScope; + try { + activeEffectScope = this; + return fn(); + } finally { + activeEffectScope = currentEffectScope; + } + } else if (this._warnOnRun) { + warn(`cannot run an inactive effect scope.`); + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + on() { + if (++this._on === 1) { + this.prevScope = activeEffectScope; + activeEffectScope = this; + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + off() { + if (this._on > 0 && --this._on === 0) { + if (activeEffectScope === this) { + activeEffectScope = this.prevScope; + } else { + let current = activeEffectScope; + while (current) { + if (current.prevScope === this) { + current.prevScope = this.prevScope; + break; + } + current = current.prevScope; + } + } + this.prevScope = void 0; + } + } + stop(fromParent) { + if (this._active) { + this._active = false; + let i, l; + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].stop(); + } + this.effects.length = 0; + for (i = 0, l = this.cleanups.length; i < l; i++) { + this.cleanups[i](); + } + this.cleanups.length = 0; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].stop(true); + } + this.scopes.length = 0; + } + if (!this.detached && this.parent && !fromParent) { + const last = this.parent.scopes.pop(); + if (last && last !== this) { + this.parent.scopes[this.index] = last; + last.index = this.index; + } + } + this.parent = void 0; + } + } +}; +function effectScope(detached) { + return new EffectScope(detached); +} +function getCurrentScope() { + return activeEffectScope; +} +function onScopeDispose(fn, failSilently = false) { + if (activeEffectScope) { + activeEffectScope.cleanups.push(fn); + } else if (!failSilently) { + warn( + `onScopeDispose() is called when there is no active effect scope to be associated with.` + ); + } +} +var activeSub; +var pausedQueueEffects = /* @__PURE__ */ new WeakSet(); +var ReactiveEffect = class { + constructor(fn) { + this.fn = fn; + this.deps = void 0; + this.depsTail = void 0; + this.flags = 1 | 4; + this.next = void 0; + this.cleanup = void 0; + this.scheduler = void 0; + if (activeEffectScope) { + if (activeEffectScope.active) { + activeEffectScope.effects.push(this); + } else { + this.flags &= -2; + } + } + } + pause() { + this.flags |= 64; + } + resume() { + if (this.flags & 64) { + this.flags &= -65; + if (pausedQueueEffects.has(this)) { + pausedQueueEffects.delete(this); + this.trigger(); + } + } + } + /** + * @internal + */ + notify() { + if (this.flags & 2 && !(this.flags & 32)) { + return; + } + if (!(this.flags & 8)) { + batch(this); + } + } + run() { + if (!(this.flags & 1)) { + return this.fn(); + } + this.flags |= 2; + cleanupEffect(this); + prepareDeps(this); + const prevEffect = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = this; + shouldTrack = true; + try { + return this.fn(); + } finally { + if (activeSub !== this) { + warn( + "Active effect was not restored correctly - this is likely a Vue internal bug." + ); + } + cleanupDeps(this); + activeSub = prevEffect; + shouldTrack = prevShouldTrack; + this.flags &= -3; + } + } + stop() { + if (this.flags & 1) { + for (let link = this.deps; link; link = link.nextDep) { + removeSub(link); + } + this.deps = this.depsTail = void 0; + cleanupEffect(this); + this.onStop && this.onStop(); + this.flags &= -2; + } + } + trigger() { + if (this.flags & 64) { + pausedQueueEffects.add(this); + } else if (this.scheduler) { + this.scheduler(); + } else { + this.runIfDirty(); + } + } + /** + * @internal + */ + runIfDirty() { + if (isDirty(this)) { + this.run(); + } + } + get dirty() { + return isDirty(this); + } +}; +var batchDepth = 0; +var batchedSub; +var batchedComputed; +function batch(sub, isComputed = false) { + sub.flags |= 8; + if (isComputed) { + sub.next = batchedComputed; + batchedComputed = sub; + return; + } + sub.next = batchedSub; + batchedSub = sub; +} +function startBatch() { + batchDepth++; +} +function endBatch() { + if (--batchDepth > 0) { + return; + } + if (batchedComputed) { + let e = batchedComputed; + batchedComputed = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + e = next; + } + } + let error; + while (batchedSub) { + let e = batchedSub; + batchedSub = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + if (e.flags & 1) { + try { + ; + e.trigger(); + } catch (err) { + if (!error) error = err; + } + } + e = next; + } + } + if (error) throw error; +} +function prepareDeps(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + link.version = -1; + link.prevActiveLink = link.dep.activeLink; + link.dep.activeLink = link; + } +} +function cleanupDeps(sub) { + let head; + let tail = sub.depsTail; + let link = tail; + while (link) { + const prev = link.prevDep; + if (link.version === -1) { + if (link === tail) tail = prev; + removeSub(link); + removeDep(link); + } else { + head = link; + } + link.dep.activeLink = link.prevActiveLink; + link.prevActiveLink = void 0; + link = prev; + } + sub.deps = head; + sub.depsTail = tail; +} +function isDirty(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + if (link.dep.version !== link.version || link.dep.computed && (refreshComputed(link.dep.computed) || link.dep.version !== link.version)) { + return true; + } + } + if (sub._dirty) { + return true; + } + return false; +} +function refreshComputed(computed3) { + if (computed3.flags & 4 && !(computed3.flags & 16)) { + return; + } + computed3.flags &= -17; + if (computed3.globalVersion === globalVersion) { + return; + } + computed3.globalVersion = globalVersion; + if (!computed3.isSSR && computed3.flags & 128 && (!computed3.deps && !computed3._dirty || !isDirty(computed3))) { + return; + } + computed3.flags |= 2; + const dep = computed3.dep; + const prevSub = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = computed3; + shouldTrack = true; + try { + prepareDeps(computed3); + const value = computed3.fn(computed3._value); + if (dep.version === 0 || hasChanged(value, computed3._value)) { + computed3.flags |= 128; + computed3._value = value; + dep.version++; + } + } catch (err) { + dep.version++; + throw err; + } finally { + activeSub = prevSub; + shouldTrack = prevShouldTrack; + cleanupDeps(computed3); + computed3.flags &= -3; + } +} +function removeSub(link, soft = false) { + const { dep, prevSub, nextSub } = link; + if (prevSub) { + prevSub.nextSub = nextSub; + link.prevSub = void 0; + } + if (nextSub) { + nextSub.prevSub = prevSub; + link.nextSub = void 0; + } + if (dep.subsHead === link) { + dep.subsHead = nextSub; + } + if (dep.subs === link) { + dep.subs = prevSub; + if (!prevSub && dep.computed) { + dep.computed.flags &= -5; + for (let l = dep.computed.deps; l; l = l.nextDep) { + removeSub(l, true); + } + } + } + if (!soft && !--dep.sc && dep.map) { + dep.map.delete(dep.key); + } +} +function removeDep(link) { + const { prevDep, nextDep } = link; + if (prevDep) { + prevDep.nextDep = nextDep; + link.prevDep = void 0; + } + if (nextDep) { + nextDep.prevDep = prevDep; + link.nextDep = void 0; + } +} +function effect(fn, options) { + if (fn.effect instanceof ReactiveEffect) { + fn = fn.effect.fn; + } + const e = new ReactiveEffect(fn); + if (options) { + extend(e, options); + } + try { + e.run(); + } catch (err) { + e.stop(); + throw err; + } + const runner = e.run.bind(e); + runner.effect = e; + return runner; +} +function stop(runner) { + runner.effect.stop(); +} +var shouldTrack = true; +var trackStack = []; +function pauseTracking() { + trackStack.push(shouldTrack); + shouldTrack = false; +} +function resetTracking() { + const last = trackStack.pop(); + shouldTrack = last === void 0 ? true : last; +} +function cleanupEffect(e) { + const { cleanup } = e; + e.cleanup = void 0; + if (cleanup) { + const prevSub = activeSub; + activeSub = void 0; + try { + cleanup(); + } finally { + activeSub = prevSub; + } + } +} +var globalVersion = 0; +var Link = class { + constructor(sub, dep) { + this.sub = sub; + this.dep = dep; + this.version = dep.version; + this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0; + } +}; +var Dep = class { + // TODO isolatedDeclarations "__v_skip" + constructor(computed3) { + this.computed = computed3; + this.version = 0; + this.activeLink = void 0; + this.subs = void 0; + this.map = void 0; + this.key = void 0; + this.sc = 0; + this.__v_skip = true; + if (true) { + this.subsHead = void 0; + } + } + track(debugInfo) { + if (!activeSub || !shouldTrack || activeSub === this.computed) { + return; + } + let link = this.activeLink; + if (link === void 0 || link.sub !== activeSub) { + link = this.activeLink = new Link(activeSub, this); + if (!activeSub.deps) { + activeSub.deps = activeSub.depsTail = link; + } else { + link.prevDep = activeSub.depsTail; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + } + addSub(link); + } else if (link.version === -1) { + link.version = this.version; + if (link.nextDep) { + const next = link.nextDep; + next.prevDep = link.prevDep; + if (link.prevDep) { + link.prevDep.nextDep = next; + } + link.prevDep = activeSub.depsTail; + link.nextDep = void 0; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + if (activeSub.deps === link) { + activeSub.deps = next; + } + } + } + if (activeSub.onTrack) { + activeSub.onTrack( + extend( + { + effect: activeSub + }, + debugInfo + ) + ); + } + return link; + } + trigger(debugInfo) { + this.version++; + globalVersion++; + this.notify(debugInfo); + } + notify(debugInfo) { + startBatch(); + try { + if (true) { + for (let head = this.subsHead; head; head = head.nextSub) { + if (head.sub.onTrigger && !(head.sub.flags & 8)) { + head.sub.onTrigger( + extend( + { + effect: head.sub + }, + debugInfo + ) + ); + } + } + } + for (let link = this.subs; link; link = link.prevSub) { + if (link.sub.notify()) { + ; + link.sub.dep.notify(); + } + } + } finally { + endBatch(); + } + } +}; +function addSub(link) { + link.dep.sc++; + if (link.sub.flags & 4) { + const computed3 = link.dep.computed; + if (computed3 && !link.dep.subs) { + computed3.flags |= 4 | 16; + for (let l = computed3.deps; l; l = l.nextDep) { + addSub(l); + } + } + const currentTail = link.dep.subs; + if (currentTail !== link) { + link.prevSub = currentTail; + if (currentTail) currentTail.nextSub = link; + } + if (link.dep.subsHead === void 0) { + link.dep.subsHead = link; + } + link.dep.subs = link; + } +} +var targetMap = /* @__PURE__ */ new WeakMap(); +var ITERATE_KEY = Symbol( + true ? "Object iterate" : "" +); +var MAP_KEY_ITERATE_KEY = Symbol( + true ? "Map keys iterate" : "" +); +var ARRAY_ITERATE_KEY = Symbol( + true ? "Array iterate" : "" +); +function track(target, type, key) { + if (shouldTrack && activeSub) { + let depsMap = targetMap.get(target); + if (!depsMap) { + targetMap.set(target, depsMap = /* @__PURE__ */ new Map()); + } + let dep = depsMap.get(key); + if (!dep) { + depsMap.set(key, dep = new Dep()); + dep.map = depsMap; + dep.key = key; + } + if (true) { + dep.track({ + target, + type, + key + }); + } else { + dep.track(); + } + } +} +function trigger(target, type, key, newValue, oldValue, oldTarget) { + const depsMap = targetMap.get(target); + if (!depsMap) { + globalVersion++; + return; + } + const run = (dep) => { + if (dep) { + if (true) { + dep.trigger({ + target, + type, + key, + newValue, + oldValue, + oldTarget + }); + } else { + dep.trigger(); + } + } + }; + startBatch(); + if (type === "clear") { + depsMap.forEach(run); + } else { + const targetIsArray = isArray(target); + const isArrayIndex = targetIsArray && isIntegerKey(key); + if (targetIsArray && key === "length") { + const newLength = Number(newValue); + depsMap.forEach((dep, key2) => { + if (key2 === "length" || key2 === ARRAY_ITERATE_KEY || !isSymbol(key2) && key2 >= newLength) { + run(dep); + } + }); + } else { + if (key !== void 0 || depsMap.has(void 0)) { + run(depsMap.get(key)); + } + if (isArrayIndex) { + run(depsMap.get(ARRAY_ITERATE_KEY)); + } + switch (type) { + case "add": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } else if (isArrayIndex) { + run(depsMap.get("length")); + } + break; + case "delete": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } + break; + case "set": + if (isMap(target)) { + run(depsMap.get(ITERATE_KEY)); + } + break; + } + } + } + endBatch(); +} +function getDepFromReactive(object, key) { + const depMap = targetMap.get(object); + return depMap && depMap.get(key); +} +function reactiveReadArray(array) { + const raw = toRaw(array); + if (raw === array) return raw; + track(raw, "iterate", ARRAY_ITERATE_KEY); + return isShallow(array) ? raw : raw.map(toReactive); +} +function shallowReadArray(arr) { + track(arr = toRaw(arr), "iterate", ARRAY_ITERATE_KEY); + return arr; +} +function toWrapped(target, item) { + if (isReadonly(target)) { + return isReactive(target) ? toReadonly(toReactive(item)) : toReadonly(item); + } + return toReactive(item); +} +var arrayInstrumentations = { + __proto__: null, + [Symbol.iterator]() { + return iterator(this, Symbol.iterator, (item) => toWrapped(this, item)); + }, + concat(...args) { + return reactiveReadArray(this).concat( + ...args.map((x) => isArray(x) ? reactiveReadArray(x) : x) + ); + }, + entries() { + return iterator(this, "entries", (value) => { + value[1] = toWrapped(this, value[1]); + return value; + }); + }, + every(fn, thisArg) { + return apply(this, "every", fn, thisArg, void 0, arguments); + }, + filter(fn, thisArg) { + return apply( + this, + "filter", + fn, + thisArg, + (v) => v.map((item) => toWrapped(this, item)), + arguments + ); + }, + find(fn, thisArg) { + return apply( + this, + "find", + fn, + thisArg, + (item) => toWrapped(this, item), + arguments + ); + }, + findIndex(fn, thisArg) { + return apply(this, "findIndex", fn, thisArg, void 0, arguments); + }, + findLast(fn, thisArg) { + return apply( + this, + "findLast", + fn, + thisArg, + (item) => toWrapped(this, item), + arguments + ); + }, + findLastIndex(fn, thisArg) { + return apply(this, "findLastIndex", fn, thisArg, void 0, arguments); + }, + // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement + forEach(fn, thisArg) { + return apply(this, "forEach", fn, thisArg, void 0, arguments); + }, + includes(...args) { + return searchProxy(this, "includes", args); + }, + indexOf(...args) { + return searchProxy(this, "indexOf", args); + }, + join(separator) { + return reactiveReadArray(this).join(separator); + }, + // keys() iterator only reads `length`, no optimization required + lastIndexOf(...args) { + return searchProxy(this, "lastIndexOf", args); + }, + map(fn, thisArg) { + return apply(this, "map", fn, thisArg, void 0, arguments); + }, + pop() { + return noTracking(this, "pop"); + }, + push(...args) { + return noTracking(this, "push", args); + }, + reduce(fn, ...args) { + return reduce(this, "reduce", fn, args); + }, + reduceRight(fn, ...args) { + return reduce(this, "reduceRight", fn, args); + }, + shift() { + return noTracking(this, "shift"); + }, + // slice could use ARRAY_ITERATE but also seems to beg for range tracking + some(fn, thisArg) { + return apply(this, "some", fn, thisArg, void 0, arguments); + }, + splice(...args) { + return noTracking(this, "splice", args); + }, + toReversed() { + return reactiveReadArray(this).toReversed(); + }, + toSorted(comparer) { + return reactiveReadArray(this).toSorted(comparer); + }, + toSpliced(...args) { + return reactiveReadArray(this).toSpliced(...args); + }, + unshift(...args) { + return noTracking(this, "unshift", args); + }, + values() { + return iterator(this, "values", (item) => toWrapped(this, item)); + } +}; +function iterator(self2, method, wrapValue) { + const arr = shallowReadArray(self2); + const iter = arr[method](); + if (arr !== self2 && !isShallow(self2)) { + iter._next = iter.next; + iter.next = () => { + const result = iter._next(); + if (!result.done) { + result.value = wrapValue(result.value); + } + return result; + }; + } + return iter; +} +var arrayProto = Array.prototype; +function apply(self2, method, fn, thisArg, wrappedRetFn, args) { + const arr = shallowReadArray(self2); + const needsWrap = arr !== self2 && !isShallow(self2); + const methodFn = arr[method]; + if (methodFn !== arrayProto[method]) { + const result2 = methodFn.apply(self2, args); + return needsWrap ? toReactive(result2) : result2; + } + let wrappedFn = fn; + if (arr !== self2) { + if (needsWrap) { + wrappedFn = function(item, index) { + return fn.call(this, toWrapped(self2, item), index, self2); + }; + } else if (fn.length > 2) { + wrappedFn = function(item, index) { + return fn.call(this, item, index, self2); + }; + } + } + const result = methodFn.call(arr, wrappedFn, thisArg); + return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result; +} +function reduce(self2, method, fn, args) { + const arr = shallowReadArray(self2); + const needsWrap = arr !== self2 && !isShallow(self2); + let wrappedFn = fn; + let wrapInitialAccumulator = false; + if (arr !== self2) { + if (needsWrap) { + wrapInitialAccumulator = args.length === 0; + wrappedFn = function(acc, item, index) { + if (wrapInitialAccumulator) { + wrapInitialAccumulator = false; + acc = toWrapped(self2, acc); + } + return fn.call(this, acc, toWrapped(self2, item), index, self2); + }; + } else if (fn.length > 3) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, item, index, self2); + }; + } + } + const result = arr[method](wrappedFn, ...args); + return wrapInitialAccumulator ? toWrapped(self2, result) : result; +} +function searchProxy(self2, method, args) { + const arr = toRaw(self2); + track(arr, "iterate", ARRAY_ITERATE_KEY); + const res = arr[method](...args); + if ((res === -1 || res === false) && isProxy(args[0])) { + args[0] = toRaw(args[0]); + return arr[method](...args); + } + return res; +} +function noTracking(self2, method, args = []) { + pauseTracking(); + startBatch(); + const res = toRaw(self2)[method].apply(self2, args); + endBatch(); + resetTracking(); + return res; +} +var isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`); +var builtInSymbols = new Set( + Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(isSymbol) +); +function hasOwnProperty2(key) { + if (!isSymbol(key)) key = String(key); + const obj = toRaw(this); + track(obj, "has", key); + return obj.hasOwnProperty(key); +} +var BaseReactiveHandler = class { + constructor(_isReadonly = false, _isShallow = false) { + this._isReadonly = _isReadonly; + this._isShallow = _isShallow; + } + get(target, key, receiver) { + if (key === "__v_skip") return target["__v_skip"]; + const isReadonly2 = this._isReadonly, isShallow2 = this._isShallow; + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_isShallow") { + return isShallow2; + } else if (key === "__v_raw") { + if (receiver === (isReadonly2 ? isShallow2 ? shallowReadonlyMap : readonlyMap : isShallow2 ? shallowReactiveMap : reactiveMap).get(target) || // receiver is not the reactive proxy, but has the same prototype + // this means the receiver is a user proxy of the reactive proxy + Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) { + return target; + } + return; + } + const targetIsArray = isArray(target); + if (!isReadonly2) { + let fn; + if (targetIsArray && (fn = arrayInstrumentations[key])) { + return fn; + } + if (key === "hasOwnProperty") { + return hasOwnProperty2; + } + } + const res = Reflect.get( + target, + key, + // if this is a proxy wrapping a ref, return methods using the raw ref + // as receiver so that we don't have to call `toRaw` on the ref in all + // its class methods + isRef2(target) ? target : receiver + ); + if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { + return res; + } + if (!isReadonly2) { + track(target, "get", key); + } + if (isShallow2) { + return res; + } + if (isRef2(res)) { + const value = targetIsArray && isIntegerKey(key) ? res : res.value; + return isReadonly2 && isObject(value) ? readonly(value) : value; + } + if (isObject(res)) { + return isReadonly2 ? readonly(res) : reactive(res); + } + return res; + } +}; +var MutableReactiveHandler = class extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(false, isShallow2); + } + set(target, key, value, receiver) { + let oldValue = target[key]; + const isArrayWithIntegerKey = isArray(target) && isIntegerKey(key); + if (!this._isShallow) { + const isOldValueReadonly = isReadonly(oldValue); + if (!isShallow(value) && !isReadonly(value)) { + oldValue = toRaw(oldValue); + value = toRaw(value); + } + if (!isArrayWithIntegerKey && isRef2(oldValue) && !isRef2(value)) { + if (isOldValueReadonly) { + if (true) { + warn( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target[key] + ); + } + return true; + } else { + oldValue.value = value; + return true; + } + } + } + const hadKey = isArrayWithIntegerKey ? Number(key) < target.length : hasOwn(target, key); + const result = Reflect.set( + target, + key, + value, + isRef2(target) ? target : receiver + ); + if (target === toRaw(receiver)) { + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + } + return result; + } + deleteProperty(target, key) { + const hadKey = hasOwn(target, key); + const oldValue = target[key]; + const result = Reflect.deleteProperty(target, key); + if (result && hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + } + has(target, key) { + const result = Reflect.has(target, key); + if (!isSymbol(key) || !builtInSymbols.has(key)) { + track(target, "has", key); + } + return result; + } + ownKeys(target) { + track( + target, + "iterate", + isArray(target) ? "length" : ITERATE_KEY + ); + return Reflect.ownKeys(target); + } +}; +var ReadonlyReactiveHandler = class extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(true, isShallow2); + } + set(target, key) { + if (true) { + warn( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } + deleteProperty(target, key) { + if (true) { + warn( + `Delete operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } +}; +var mutableHandlers = new MutableReactiveHandler(); +var readonlyHandlers = new ReadonlyReactiveHandler(); +var shallowReactiveHandlers = new MutableReactiveHandler(true); +var shallowReadonlyHandlers = new ReadonlyReactiveHandler(true); +var toShallow = (value) => value; +var getProto = (v) => Reflect.getPrototypeOf(v); +function createIterableMethod(method, isReadonly2, isShallow2) { + return function(...args) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const targetIsMap = isMap(rawTarget); + const isPair = method === "entries" || method === Symbol.iterator && targetIsMap; + const isKeyOnly = method === "keys" && targetIsMap; + const innerIterator = target[method](...args); + const wrap = isShallow2 ? toShallow : isReadonly2 ? toReadonly : toReactive; + !isReadonly2 && track( + rawTarget, + "iterate", + isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY + ); + return extend( + // inheriting all iterator properties + Object.create(innerIterator), + { + // iterator protocol + next() { + const { value, done } = innerIterator.next(); + return done ? { value, done } : { + value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), + done + }; + } + } + ); + }; +} +function createReadonlyMethod(type) { + return function(...args) { + if (true) { + const key = args[0] ? `on key "${args[0]}" ` : ``; + warn( + `${capitalize(type)} operation ${key}failed: target is readonly.`, + toRaw(this) + ); + } + return type === "delete" ? false : type === "clear" ? void 0 : this; + }; +} +function createInstrumentations(readonly2, shallow) { + const instrumentations = { + get(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "get", key); + } + track(rawTarget, "get", rawKey); + } + const { has } = getProto(rawTarget); + const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive; + if (has.call(rawTarget, key)) { + return wrap(target.get(key)); + } else if (has.call(rawTarget, rawKey)) { + return wrap(target.get(rawKey)); + } else if (target !== rawTarget) { + target.get(key); + } + }, + get size() { + const target = this["__v_raw"]; + !readonly2 && track(toRaw(target), "iterate", ITERATE_KEY); + return target.size; + }, + has(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "has", key); + } + track(rawTarget, "has", rawKey); + } + return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey); + }, + forEach(callback, thisArg) { + const observed = this; + const target = observed["__v_raw"]; + const rawTarget = toRaw(target); + const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive; + !readonly2 && track(rawTarget, "iterate", ITERATE_KEY); + return target.forEach((value, key) => { + return callback.call(thisArg, wrap(value), wrap(key), observed); + }); + } + }; + extend( + instrumentations, + readonly2 ? { + add: createReadonlyMethod("add"), + set: createReadonlyMethod("set"), + delete: createReadonlyMethod("delete"), + clear: createReadonlyMethod("clear") + } : { + add(value) { + const target = toRaw(this); + const proto = getProto(target); + const rawValue = toRaw(value); + const valueToAdd = !shallow && !isShallow(value) && !isReadonly(value) ? rawValue : value; + const hadKey = proto.has.call(target, valueToAdd) || hasChanged(value, valueToAdd) && proto.has.call(target, value) || hasChanged(rawValue, valueToAdd) && proto.has.call(target, rawValue); + if (!hadKey) { + target.add(valueToAdd); + trigger(target, "add", valueToAdd, valueToAdd); + } + return this; + }, + set(key, value) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value); + } + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else if (true) { + checkIdentityKeys(target, has, key); + } + const oldValue = get.call(target, key); + target.set(key, value); + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + return this; + }, + delete(key) { + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else if (true) { + checkIdentityKeys(target, has, key); + } + const oldValue = get ? get.call(target, key) : void 0; + const result = target.delete(key); + if (hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + }, + clear() { + const target = toRaw(this); + const hadItems = target.size !== 0; + const oldTarget = true ? isMap(target) ? new Map(target) : new Set(target) : void 0; + const result = target.clear(); + if (hadItems) { + trigger( + target, + "clear", + void 0, + void 0, + oldTarget + ); + } + return result; + } + } + ); + const iteratorMethods = [ + "keys", + "values", + "entries", + Symbol.iterator + ]; + iteratorMethods.forEach((method) => { + instrumentations[method] = createIterableMethod(method, readonly2, shallow); + }); + return instrumentations; +} +function createInstrumentationGetter(isReadonly2, shallow) { + const instrumentations = createInstrumentations(isReadonly2, shallow); + return (target, key, receiver) => { + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_raw") { + return target; + } + return Reflect.get( + hasOwn(instrumentations, key) && key in target ? instrumentations : target, + key, + receiver + ); + }; +} +var mutableCollectionHandlers = { + get: createInstrumentationGetter(false, false) +}; +var shallowCollectionHandlers = { + get: createInstrumentationGetter(false, true) +}; +var readonlyCollectionHandlers = { + get: createInstrumentationGetter(true, false) +}; +var shallowReadonlyCollectionHandlers = { + get: createInstrumentationGetter(true, true) +}; +function checkIdentityKeys(target, has, key) { + const rawKey = toRaw(key); + if (rawKey !== key && has.call(target, rawKey)) { + const type = toRawType(target); + warn( + `Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.` + ); + } +} +var reactiveMap = /* @__PURE__ */ new WeakMap(); +var shallowReactiveMap = /* @__PURE__ */ new WeakMap(); +var readonlyMap = /* @__PURE__ */ new WeakMap(); +var shallowReadonlyMap = /* @__PURE__ */ new WeakMap(); +function targetTypeMap(rawType) { + switch (rawType) { + case "Object": + case "Array": + return 1; + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": + return 2; + default: + return 0; + } +} +function getTargetType(value) { + return value["__v_skip"] || !Object.isExtensible(value) ? 0 : targetTypeMap(toRawType(value)); +} +function reactive(target) { + if (isReadonly(target)) { + return target; + } + return createReactiveObject( + target, + false, + mutableHandlers, + mutableCollectionHandlers, + reactiveMap + ); +} +function shallowReactive(target) { + return createReactiveObject( + target, + false, + shallowReactiveHandlers, + shallowCollectionHandlers, + shallowReactiveMap + ); +} +function readonly(target) { + return createReactiveObject( + target, + true, + readonlyHandlers, + readonlyCollectionHandlers, + readonlyMap + ); +} +function shallowReadonly(target) { + return createReactiveObject( + target, + true, + shallowReadonlyHandlers, + shallowReadonlyCollectionHandlers, + shallowReadonlyMap + ); +} +function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) { + if (!isObject(target)) { + if (true) { + warn( + `value cannot be made ${isReadonly2 ? "readonly" : "reactive"}: ${String( + target + )}` + ); + } + return target; + } + if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) { + return target; + } + const targetType = getTargetType(target); + if (targetType === 0) { + return target; + } + const existingProxy = proxyMap.get(target); + if (existingProxy) { + return existingProxy; + } + const proxy = new Proxy( + target, + targetType === 2 ? collectionHandlers : baseHandlers + ); + proxyMap.set(target, proxy); + return proxy; +} +function isReactive(value) { + if (isReadonly(value)) { + return isReactive(value["__v_raw"]); + } + return !!(value && value["__v_isReactive"]); +} +function isReadonly(value) { + return !!(value && value["__v_isReadonly"]); +} +function isShallow(value) { + return !!(value && value["__v_isShallow"]); +} +function isProxy(value) { + return value ? !!value["__v_raw"] : false; +} +function toRaw(observed) { + const raw = observed && observed["__v_raw"]; + return raw ? toRaw(raw) : observed; +} +function markRaw(value) { + if (!hasOwn(value, "__v_skip") && Object.isExtensible(value)) { + def(value, "__v_skip", true); + } + return value; +} +var toReactive = (value) => isObject(value) ? reactive(value) : value; +var toReadonly = (value) => isObject(value) ? readonly(value) : value; +function isRef2(r) { + return r ? r["__v_isRef"] === true : false; +} +function ref(value) { + return createRef(value, false); +} +function shallowRef(value) { + return createRef(value, true); +} +function createRef(rawValue, shallow) { + if (isRef2(rawValue)) { + return rawValue; + } + return new RefImpl(rawValue, shallow); +} +var RefImpl = class { + constructor(value, isShallow2) { + this.dep = new Dep(); + this["__v_isRef"] = true; + this["__v_isShallow"] = false; + this._rawValue = isShallow2 ? value : toRaw(value); + this._value = isShallow2 ? value : toReactive(value); + this["__v_isShallow"] = isShallow2; + } + get value() { + if (true) { + this.dep.track({ + target: this, + type: "get", + key: "value" + }); + } else { + this.dep.track(); + } + return this._value; + } + set value(newValue) { + const oldValue = this._rawValue; + const useDirectValue = this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue); + newValue = useDirectValue ? newValue : toRaw(newValue); + if (hasChanged(newValue, oldValue)) { + this._rawValue = newValue; + this._value = useDirectValue ? newValue : toReactive(newValue); + if (true) { + this.dep.trigger({ + target: this, + type: "set", + key: "value", + newValue, + oldValue + }); + } else { + this.dep.trigger(); + } + } + } +}; +function triggerRef(ref2) { + if (ref2.dep) { + if (true) { + ref2.dep.trigger({ + target: ref2, + type: "set", + key: "value", + newValue: ref2._value + }); + } else { + ref2.dep.trigger(); + } + } +} +function unref(ref2) { + return isRef2(ref2) ? ref2.value : ref2; +} +function toValue(source) { + return isFunction(source) ? source() : unref(source); +} +var shallowUnwrapHandlers = { + get: (target, key, receiver) => key === "__v_raw" ? target : unref(Reflect.get(target, key, receiver)), + set: (target, key, value, receiver) => { + const oldValue = target[key]; + if (isRef2(oldValue) && !isRef2(value)) { + oldValue.value = value; + return true; + } else { + return Reflect.set(target, key, value, receiver); + } + } +}; +function proxyRefs(objectWithRefs) { + return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers); +} +var CustomRefImpl = class { + constructor(factory) { + this["__v_isRef"] = true; + this._value = void 0; + const dep = this.dep = new Dep(); + const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep)); + this._get = get; + this._set = set; + } + get value() { + return this._value = this._get(); + } + set value(newVal) { + this._set(newVal); + } +}; +function customRef(factory) { + return new CustomRefImpl(factory); +} +function toRefs(object) { + if (!isProxy(object)) { + warn(`toRefs() expects a reactive object but received a plain one.`); + } + const ret = isArray(object) ? new Array(object.length) : {}; + for (const key in object) { + ret[key] = propertyToRef(object, key); + } + return ret; +} +var ObjectRefImpl = class { + constructor(_object, key, _defaultValue) { + this._object = _object; + this._defaultValue = _defaultValue; + this["__v_isRef"] = true; + this._value = void 0; + this._key = isSymbol(key) ? key : String(key); + this._raw = toRaw(_object); + let shallow = true; + let obj = _object; + if (!isArray(_object) || isSymbol(this._key) || !isIntegerKey(this._key)) { + do { + shallow = !isProxy(obj) || isShallow(obj); + } while (shallow && (obj = obj["__v_raw"])); + } + this._shallow = shallow; + } + get value() { + let val = this._object[this._key]; + if (this._shallow) { + val = unref(val); + } + return this._value = val === void 0 ? this._defaultValue : val; + } + set value(newVal) { + if (this._shallow && isRef2(this._raw[this._key])) { + const nestedRef = this._object[this._key]; + if (isRef2(nestedRef)) { + nestedRef.value = newVal; + return; + } + } + this._object[this._key] = newVal; + } + get dep() { + return getDepFromReactive(this._raw, this._key); + } +}; +var GetterRefImpl = class { + constructor(_getter) { + this._getter = _getter; + this["__v_isRef"] = true; + this["__v_isReadonly"] = true; + this._value = void 0; + } + get value() { + return this._value = this._getter(); + } +}; +function toRef(source, key, defaultValue) { + if (isRef2(source)) { + return source; + } else if (isFunction(source)) { + return new GetterRefImpl(source); + } else if (isObject(source) && arguments.length > 1) { + return propertyToRef(source, key, defaultValue); + } else { + return ref(source); + } +} +function propertyToRef(source, key, defaultValue) { + return new ObjectRefImpl(source, key, defaultValue); +} +var ComputedRefImpl = class { + constructor(fn, setter, isSSR) { + this.fn = fn; + this.setter = setter; + this._value = void 0; + this.dep = new Dep(this); + this.__v_isRef = true; + this.deps = void 0; + this.depsTail = void 0; + this.flags = 16; + this.globalVersion = globalVersion - 1; + this.next = void 0; + this.effect = this; + this["__v_isReadonly"] = !setter; + this.isSSR = isSSR; + } + /** + * @internal + */ + notify() { + this.flags |= 16; + if (!(this.flags & 8) && // avoid infinite self recursion + activeSub !== this) { + batch(this, true); + return true; + } else if (true) ; + } + get value() { + const link = true ? this.dep.track({ + target: this, + type: "get", + key: "value" + }) : this.dep.track(); + refreshComputed(this); + if (link) { + link.version = this.dep.version; + } + return this._value; + } + set value(newValue) { + if (this.setter) { + this.setter(newValue); + } else if (true) { + warn("Write operation failed: computed value is readonly"); + } + } +}; +function computed(getterOrOptions, debugOptions, isSSR = false) { + let getter; + let setter; + if (isFunction(getterOrOptions)) { + getter = getterOrOptions; + } else { + getter = getterOrOptions.get; + setter = getterOrOptions.set; + } + const cRef = new ComputedRefImpl(getter, setter, isSSR); + if (debugOptions && !isSSR) { + cRef.onTrack = debugOptions.onTrack; + cRef.onTrigger = debugOptions.onTrigger; + } + return cRef; +} +var TrackOpTypes = { + "GET": "get", + "HAS": "has", + "ITERATE": "iterate" +}; +var TriggerOpTypes = { + "SET": "set", + "ADD": "add", + "DELETE": "delete", + "CLEAR": "clear" +}; +var INITIAL_WATCHER_VALUE = {}; +var cleanupMap = /* @__PURE__ */ new WeakMap(); +var activeWatcher = void 0; +function getCurrentWatcher() { + return activeWatcher; +} +function onWatcherCleanup(cleanupFn, failSilently = false, owner = activeWatcher) { + if (owner) { + let cleanups = cleanupMap.get(owner); + if (!cleanups) cleanupMap.set(owner, cleanups = []); + cleanups.push(cleanupFn); + } else if (!failSilently) { + warn( + `onWatcherCleanup() was called when there was no active watcher to associate with.` + ); + } +} +function watch(source, cb, options = EMPTY_OBJ) { + const { immediate, deep, once, scheduler, augmentJob, call } = options; + const warnInvalidSource = (s) => { + (options.onWarn || warn)( + `Invalid watch source: `, + s, + `A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.` + ); + }; + const reactiveGetter = (source2) => { + if (deep) return source2; + if (isShallow(source2) || deep === false || deep === 0) + return traverse(source2, 1); + return traverse(source2); + }; + let effect2; + let getter; + let cleanup; + let boundCleanup; + let forceTrigger = false; + let isMultiSource = false; + if (isRef2(source)) { + getter = () => source.value; + forceTrigger = isShallow(source); + } else if (isReactive(source)) { + getter = () => reactiveGetter(source); + forceTrigger = true; + } else if (isArray(source)) { + isMultiSource = true; + forceTrigger = source.some((s) => isReactive(s) || isShallow(s)); + getter = () => source.map((s) => { + if (isRef2(s)) { + return s.value; + } else if (isReactive(s)) { + return reactiveGetter(s); + } else if (isFunction(s)) { + return call ? call(s, 2) : s(); + } else { + warnInvalidSource(s); + } + }); + } else if (isFunction(source)) { + if (cb) { + getter = call ? () => call(source, 2) : source; + } else { + getter = () => { + if (cleanup) { + pauseTracking(); + try { + cleanup(); + } finally { + resetTracking(); + } + } + const currentEffect = activeWatcher; + activeWatcher = effect2; + try { + return call ? call(source, 3, [boundCleanup]) : source(boundCleanup); + } finally { + activeWatcher = currentEffect; + } + }; + } + } else { + getter = NOOP; + warnInvalidSource(source); + } + if (cb && deep) { + const baseGetter = getter; + const depth = deep === true ? Infinity : deep; + getter = () => traverse(baseGetter(), depth); + } + const scope = getCurrentScope(); + const watchHandle = () => { + effect2.stop(); + if (scope && scope.active) { + remove(scope.effects, effect2); + } + }; + if (once && cb) { + const _cb = cb; + cb = (...args) => { + _cb(...args); + watchHandle(); + }; + } + let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE; + const job = (immediateFirstRun) => { + if (!(effect2.flags & 1) || !effect2.dirty && !immediateFirstRun) { + return; + } + if (cb) { + const newValue = effect2.run(); + if (deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue))) { + if (cleanup) { + cleanup(); + } + const currentWatcher = activeWatcher; + activeWatcher = effect2; + try { + const args = [ + newValue, + // pass undefined as the old value when it's changed for the first time + oldValue === INITIAL_WATCHER_VALUE ? void 0 : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, + boundCleanup + ]; + oldValue = newValue; + call ? call(cb, 3, args) : ( + // @ts-expect-error + cb(...args) + ); + } finally { + activeWatcher = currentWatcher; + } + } + } else { + effect2.run(); + } + }; + if (augmentJob) { + augmentJob(job); + } + effect2 = new ReactiveEffect(getter); + effect2.scheduler = scheduler ? () => scheduler(job, false) : job; + boundCleanup = (fn) => onWatcherCleanup(fn, false, effect2); + cleanup = effect2.onStop = () => { + const cleanups = cleanupMap.get(effect2); + if (cleanups) { + if (call) { + call(cleanups, 4); + } else { + for (const cleanup2 of cleanups) cleanup2(); + } + cleanupMap.delete(effect2); + } + }; + if (true) { + effect2.onTrack = options.onTrack; + effect2.onTrigger = options.onTrigger; + } + if (cb) { + if (immediate) { + job(true); + } else { + oldValue = effect2.run(); + } + } else if (scheduler) { + scheduler(job.bind(null, true), true); + } else { + effect2.run(); + } + watchHandle.pause = effect2.pause.bind(effect2); + watchHandle.resume = effect2.resume.bind(effect2); + watchHandle.stop = watchHandle; + return watchHandle; +} +function traverse(value, depth = Infinity, seen) { + if (depth <= 0 || !isObject(value) || value["__v_skip"]) { + return value; + } + seen = seen || /* @__PURE__ */ new Map(); + if ((seen.get(value) || 0) >= depth) { + return value; + } + seen.set(value, depth); + depth--; + if (isRef2(value)) { + traverse(value.value, depth, seen); + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], depth, seen); + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v) => { + traverse(v, depth, seen); + }); + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], depth, seen); + } + for (const key of Object.getOwnPropertySymbols(value)) { + if (Object.prototype.propertyIsEnumerable.call(value, key)) { + traverse(value[key], depth, seen); + } + } + } + return value; +} + +// node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js +var stack = []; +function pushWarningContext(vnode) { + stack.push(vnode); +} +function popWarningContext() { + stack.pop(); +} +var isWarning = false; +function warn$1(msg, ...args) { + if (isWarning) return; + isWarning = true; + pauseTracking(); + const instance = stack.length ? stack[stack.length - 1].component : null; + const appWarnHandler = instance && instance.appContext.config.warnHandler; + const trace = getComponentTrace(); + if (appWarnHandler) { + callWithErrorHandling( + appWarnHandler, + instance, + 11, + [ + // eslint-disable-next-line no-restricted-syntax + msg + args.map((a) => { + var _a, _b; + return (_b = (_a = a.toString) == null ? void 0 : _a.call(a)) != null ? _b : JSON.stringify(a); + }).join(""), + instance && instance.proxy, + trace.map( + ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>` + ).join("\n"), + trace + ] + ); + } else { + const warnArgs = [`[Vue warn]: ${msg}`, ...args]; + if (trace.length && // avoid spamming console during tests + true) { + warnArgs.push(` +`, ...formatTrace(trace)); + } + console.warn(...warnArgs); + } + resetTracking(); + isWarning = false; +} +function getComponentTrace() { + let currentVNode = stack[stack.length - 1]; + if (!currentVNode) { + return []; + } + const normalizedStack = []; + while (currentVNode) { + const last = normalizedStack[0]; + if (last && last.vnode === currentVNode) { + last.recurseCount++; + } else { + normalizedStack.push({ + vnode: currentVNode, + recurseCount: 0 + }); + } + const parentInstance = currentVNode.component && currentVNode.component.parent; + currentVNode = parentInstance && parentInstance.vnode; + } + return normalizedStack; +} +function formatTrace(trace) { + const logs = []; + trace.forEach((entry, i) => { + logs.push(...i === 0 ? [] : [` +`], ...formatTraceEntry(entry)); + }); + return logs; +} +function formatTraceEntry({ vnode, recurseCount }) { + const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``; + const isRoot = vnode.component ? vnode.component.parent == null : false; + const open = ` at <${formatComponentName( + vnode.component, + vnode.type, + isRoot + )}`; + const close = `>` + postfix; + return vnode.props ? [open, ...formatProps(vnode.props), close] : [open + close]; +} +function formatProps(props) { + const res = []; + const keys = Object.keys(props); + keys.slice(0, 3).forEach((key) => { + res.push(...formatProp(key, props[key])); + }); + if (keys.length > 3) { + res.push(` ...`); + } + return res; +} +function formatProp(key, value, raw) { + if (isString(value)) { + value = JSON.stringify(value); + return raw ? value : [`${key}=${value}`]; + } else if (typeof value === "number" || typeof value === "boolean" || value == null) { + return raw ? value : [`${key}=${value}`]; + } else if (isRef2(value)) { + value = formatProp(key, toRaw(value.value), true); + return raw ? value : [`${key}=Ref<`, value, `>`]; + } else if (isFunction(value)) { + return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]; + } else { + value = toRaw(value); + return raw ? value : [`${key}=`, value]; + } +} +function assertNumber(val, type) { + if (false) return; + if (val === void 0) { + return; + } else if (typeof val !== "number") { + warn$1(`${type} is not a valid number - got ${JSON.stringify(val)}.`); + } else if (isNaN(val)) { + warn$1(`${type} is NaN - the duration expression might be incorrect.`); + } +} +var ErrorCodes = { + "SETUP_FUNCTION": 0, + "0": "SETUP_FUNCTION", + "RENDER_FUNCTION": 1, + "1": "RENDER_FUNCTION", + "NATIVE_EVENT_HANDLER": 5, + "5": "NATIVE_EVENT_HANDLER", + "COMPONENT_EVENT_HANDLER": 6, + "6": "COMPONENT_EVENT_HANDLER", + "VNODE_HOOK": 7, + "7": "VNODE_HOOK", + "DIRECTIVE_HOOK": 8, + "8": "DIRECTIVE_HOOK", + "TRANSITION_HOOK": 9, + "9": "TRANSITION_HOOK", + "APP_ERROR_HANDLER": 10, + "10": "APP_ERROR_HANDLER", + "APP_WARN_HANDLER": 11, + "11": "APP_WARN_HANDLER", + "FUNCTION_REF": 12, + "12": "FUNCTION_REF", + "ASYNC_COMPONENT_LOADER": 13, + "13": "ASYNC_COMPONENT_LOADER", + "SCHEDULER": 14, + "14": "SCHEDULER", + "COMPONENT_UPDATE": 15, + "15": "COMPONENT_UPDATE", + "APP_UNMOUNT_CLEANUP": 16, + "16": "APP_UNMOUNT_CLEANUP" +}; +var ErrorTypeStrings$1 = { + ["sp"]: "serverPrefetch hook", + ["bc"]: "beforeCreate hook", + ["c"]: "created hook", + ["bm"]: "beforeMount hook", + ["m"]: "mounted hook", + ["bu"]: "beforeUpdate hook", + ["u"]: "updated", + ["bum"]: "beforeUnmount hook", + ["um"]: "unmounted hook", + ["a"]: "activated hook", + ["da"]: "deactivated hook", + ["ec"]: "errorCaptured hook", + ["rtc"]: "renderTracked hook", + ["rtg"]: "renderTriggered hook", + [0]: "setup function", + [1]: "render function", + [2]: "watcher getter", + [3]: "watcher callback", + [4]: "watcher cleanup function", + [5]: "native event handler", + [6]: "component event handler", + [7]: "vnode hook", + [8]: "directive hook", + [9]: "transition hook", + [10]: "app errorHandler", + [11]: "app warnHandler", + [12]: "ref function", + [13]: "async component loader", + [14]: "scheduler flush", + [15]: "component update", + [16]: "app unmount cleanup function" +}; +function callWithErrorHandling(fn, instance, type, args) { + try { + return args ? fn(...args) : fn(); + } catch (err) { + handleError(err, instance, type); + } +} +function callWithAsyncErrorHandling(fn, instance, type, args) { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, instance, type, args); + if (res && isPromise(res)) { + res.catch((err) => { + handleError(err, instance, type); + }); + } + return res; + } + if (isArray(fn)) { + const values = []; + for (let i = 0; i < fn.length; i++) { + values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)); + } + return values; + } else if (true) { + warn$1( + `Invalid value type passed to callWithAsyncErrorHandling(): ${typeof fn}` + ); + } +} +function handleError(err, instance, type, throwInDev = true) { + const contextVNode = instance ? instance.vnode : null; + const { errorHandler, throwUnhandledErrorInProduction } = instance && instance.appContext.config || EMPTY_OBJ; + if (instance) { + let cur = instance.parent; + const exposedInstance = instance.proxy; + const errorInfo = true ? ErrorTypeStrings$1[type] : `https://vuejs.org/error-reference/#runtime-${type}`; + while (cur) { + const errorCapturedHooks = cur.ec; + if (errorCapturedHooks) { + for (let i = 0; i < errorCapturedHooks.length; i++) { + if (errorCapturedHooks[i](err, exposedInstance, errorInfo) === false) { + return; + } + } + } + cur = cur.parent; + } + if (errorHandler) { + pauseTracking(); + callWithErrorHandling(errorHandler, null, 10, [ + err, + exposedInstance, + errorInfo + ]); + resetTracking(); + return; + } + } + logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction); +} +function logError(err, type, contextVNode, throwInDev = true, throwInProd = false) { + if (true) { + const info = ErrorTypeStrings$1[type]; + if (contextVNode) { + pushWarningContext(contextVNode); + } + warn$1(`Unhandled error${info ? ` during execution of ${info}` : ``}`); + if (contextVNode) { + popWarningContext(); + } + if (throwInDev) { + throw err; + } else { + console.error(err); + } + } else if (throwInProd) { + throw err; + } else { + console.error(err); + } +} +var queue = []; +var flushIndex = -1; +var pendingPostFlushCbs = []; +var activePostFlushCbs = null; +var postFlushIndex = 0; +var resolvedPromise = Promise.resolve(); +var currentFlushPromise = null; +var RECURSION_LIMIT = 100; +function nextTick(fn) { + const p2 = currentFlushPromise || resolvedPromise; + return fn ? p2.then(this ? fn.bind(this) : fn) : p2; +} +function findInsertionIndex(id) { + let start = flushIndex + 1; + let end = queue.length; + while (start < end) { + const middle = start + end >>> 1; + const middleJob = queue[middle]; + const middleJobId = getId(middleJob); + if (middleJobId < id || middleJobId === id && middleJob.flags & 2) { + start = middle + 1; + } else { + end = middle; + } + } + return start; +} +function queueJob(job) { + if (!(job.flags & 1)) { + const jobId = getId(job); + const lastJob = queue[queue.length - 1]; + if (!lastJob || // fast path when the job id is larger than the tail + !(job.flags & 2) && jobId >= getId(lastJob)) { + queue.push(job); + } else { + queue.splice(findInsertionIndex(jobId), 0, job); + } + job.flags |= 1; + queueFlush(); + } +} +function queueFlush() { + if (!currentFlushPromise) { + currentFlushPromise = resolvedPromise.then(flushJobs); + } +} +function queuePostFlushCb(cb) { + if (!isArray(cb)) { + if (activePostFlushCbs && cb.id === -1) { + activePostFlushCbs.splice(postFlushIndex + 1, 0, cb); + } else if (!(cb.flags & 1)) { + pendingPostFlushCbs.push(cb); + cb.flags |= 1; + } + } else { + pendingPostFlushCbs.push(...cb); + } + queueFlush(); +} +function flushPreFlushCbs(instance, seen, i = flushIndex + 1) { + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + for (; i < queue.length; i++) { + const cb = queue[i]; + if (cb && cb.flags & 2) { + if (instance && cb.id !== instance.uid) { + continue; + } + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + queue.splice(i, 1); + i--; + if (cb.flags & 4) { + cb.flags &= -2; + } + cb(); + if (!(cb.flags & 4)) { + cb.flags &= -2; + } + } + } +} +function flushPostFlushCbs(seen) { + if (pendingPostFlushCbs.length) { + const deduped = [...new Set(pendingPostFlushCbs)].sort( + (a, b) => getId(a) - getId(b) + ); + pendingPostFlushCbs.length = 0; + if (activePostFlushCbs) { + activePostFlushCbs.push(...deduped); + return; + } + activePostFlushCbs = deduped; + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) { + const cb = activePostFlushCbs[postFlushIndex]; + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + if (cb.flags & 4) { + cb.flags &= -2; + } + if (!(cb.flags & 8)) cb(); + cb.flags &= -2; + } + activePostFlushCbs = null; + postFlushIndex = 0; + } +} +var getId = (job) => job.id == null ? job.flags & 2 ? -1 : Infinity : job.id; +function flushJobs(seen) { + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + const check = true ? (job) => checkRecursiveUpdates(seen, job) : NOOP; + try { + for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job && !(job.flags & 8)) { + if (check(job)) { + continue; + } + if (job.flags & 4) { + job.flags &= ~1; + } + callWithErrorHandling( + job, + job.i, + job.i ? 15 : 14 + ); + if (!(job.flags & 4)) { + job.flags &= ~1; + } + } + } + } finally { + for (; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job) { + job.flags &= -2; + } + } + flushIndex = -1; + queue.length = 0; + flushPostFlushCbs(seen); + currentFlushPromise = null; + if (queue.length || pendingPostFlushCbs.length) { + flushJobs(seen); + } + } +} +function checkRecursiveUpdates(seen, fn) { + const count = seen.get(fn) || 0; + if (count > RECURSION_LIMIT) { + const instance = fn.i; + const componentName = instance && getComponentName(instance.type); + handleError( + `Maximum recursive updates exceeded${componentName ? ` in component <${componentName}>` : ``}. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.`, + null, + 10 + ); + return true; + } + seen.set(fn, count + 1); + return false; +} +var isHmrUpdating = false; +var setHmrUpdating = (v) => { + try { + return isHmrUpdating; + } finally { + isHmrUpdating = v; + } +}; +var hmrDirtyComponents = /* @__PURE__ */ new Map(); +if (true) { + getGlobalThis().__VUE_HMR_RUNTIME__ = { + createRecord: tryWrap(createRecord), + rerender: tryWrap(rerender), + reload: tryWrap(reload) + }; +} +var map = /* @__PURE__ */ new Map(); +function registerHMR(instance) { + const id = instance.type.__hmrId; + let record = map.get(id); + if (!record) { + createRecord(id, instance.type); + record = map.get(id); + } + record.instances.add(instance); +} +function unregisterHMR(instance) { + map.get(instance.type.__hmrId).instances.delete(instance); +} +function createRecord(id, initialDef) { + if (map.has(id)) { + return false; + } + map.set(id, { + initialDef: normalizeClassComponent(initialDef), + instances: /* @__PURE__ */ new Set() + }); + return true; +} +function normalizeClassComponent(component) { + return isClassComponent(component) ? component.__vccOpts : component; +} +function rerender(id, newRender) { + const record = map.get(id); + if (!record) { + return; + } + record.initialDef.render = newRender; + [...record.instances].forEach((instance) => { + if (newRender) { + instance.render = newRender; + normalizeClassComponent(instance.type).render = newRender; + } + instance.renderCache = []; + isHmrUpdating = true; + if (!(instance.job.flags & 8)) { + instance.update(); + } + isHmrUpdating = false; + }); +} +function reload(id, newComp) { + const record = map.get(id); + if (!record) return; + newComp = normalizeClassComponent(newComp); + updateComponentDef(record.initialDef, newComp); + const instances = [...record.instances]; + for (let i = 0; i < instances.length; i++) { + const instance = instances[i]; + const oldComp = normalizeClassComponent(instance.type); + let dirtyInstances = hmrDirtyComponents.get(oldComp); + if (!dirtyInstances) { + if (oldComp !== record.initialDef) { + updateComponentDef(oldComp, newComp); + } + hmrDirtyComponents.set(oldComp, dirtyInstances = /* @__PURE__ */ new Set()); + } + dirtyInstances.add(instance); + instance.appContext.propsCache.delete(instance.type); + instance.appContext.emitsCache.delete(instance.type); + instance.appContext.optionsCache.delete(instance.type); + if (instance.ceReload) { + dirtyInstances.add(instance); + instance.ceReload(newComp.styles); + dirtyInstances.delete(instance); + } else if (instance.parent) { + queueJob(() => { + if (!(instance.job.flags & 8)) { + isHmrUpdating = true; + instance.parent.update(); + isHmrUpdating = false; + dirtyInstances.delete(instance); + } + }); + } else if (instance.appContext.reload) { + instance.appContext.reload(); + } else if (typeof window !== "undefined") { + window.location.reload(); + } else { + console.warn( + "[HMR] Root or manually mounted instance modified. Full reload required." + ); + } + if (instance.root.ce && instance !== instance.root) { + instance.root.ce._removeChildStyle(oldComp); + } + } + queuePostFlushCb(() => { + hmrDirtyComponents.clear(); + }); +} +function updateComponentDef(oldComp, newComp) { + extend(oldComp, newComp); + for (const key in oldComp) { + if (key !== "__file" && !(key in newComp)) { + delete oldComp[key]; + } + } +} +function tryWrap(fn) { + return (id, arg) => { + try { + return fn(id, arg); + } catch (e) { + console.error(e); + console.warn( + `[HMR] Something went wrong during Vue component hot-reload. Full reload required.` + ); + } + }; +} +var devtools$1; +var buffer = []; +var devtoolsNotInstalled = false; +function emit$1(event, ...args) { + if (devtools$1) { + devtools$1.emit(event, ...args); + } else if (!devtoolsNotInstalled) { + buffer.push({ event, args }); + } +} +function setDevtoolsHook$1(hook, target) { + var _a, _b; + devtools$1 = hook; + if (devtools$1) { + devtools$1.enabled = true; + buffer.forEach(({ event, args }) => devtools$1.emit(event, ...args)); + buffer = []; + } else if ( + // handle late devtools injection - only do this if we are in an actual + // browser environment to avoid the timer handle stalling test runner exit + // (#4815) + typeof window !== "undefined" && // some envs mock window but not fully + window.HTMLElement && // also exclude jsdom + // eslint-disable-next-line no-restricted-syntax + !((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null ? void 0 : _b.includes("jsdom")) + ) { + const replay = target.__VUE_DEVTOOLS_HOOK_REPLAY__ = target.__VUE_DEVTOOLS_HOOK_REPLAY__ || []; + replay.push((newHook) => { + setDevtoolsHook$1(newHook, target); + }); + setTimeout(() => { + if (!devtools$1) { + target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null; + devtoolsNotInstalled = true; + buffer = []; + } + }, 3e3); + } else { + devtoolsNotInstalled = true; + buffer = []; + } +} +function devtoolsInitApp(app, version2) { + emit$1("app:init", app, version2, { + Fragment, + Text, + Comment, + Static + }); +} +function devtoolsUnmountApp(app) { + emit$1("app:unmount", app); +} +var devtoolsComponentAdded = createDevtoolsComponentHook( + "component:added" + /* COMPONENT_ADDED */ +); +var devtoolsComponentUpdated = createDevtoolsComponentHook( + "component:updated" + /* COMPONENT_UPDATED */ +); +var _devtoolsComponentRemoved = createDevtoolsComponentHook( + "component:removed" + /* COMPONENT_REMOVED */ +); +var devtoolsComponentRemoved = (component) => { + if (devtools$1 && typeof devtools$1.cleanupBuffer === "function" && // remove the component if it wasn't buffered + !devtools$1.cleanupBuffer(component)) { + _devtoolsComponentRemoved(component); + } +}; +function createDevtoolsComponentHook(hook) { + return (component) => { + emit$1( + hook, + component.appContext.app, + component.uid, + component.parent ? component.parent.uid : void 0, + component + ); + }; +} +var devtoolsPerfStart = createDevtoolsPerformanceHook( + "perf:start" + /* PERFORMANCE_START */ +); +var devtoolsPerfEnd = createDevtoolsPerformanceHook( + "perf:end" + /* PERFORMANCE_END */ +); +function createDevtoolsPerformanceHook(hook) { + return (component, type, time) => { + emit$1(hook, component.appContext.app, component.uid, component, type, time); + }; +} +function devtoolsComponentEmit(component, event, params) { + emit$1( + "component:emit", + component.appContext.app, + component, + event, + params + ); +} +var currentRenderingInstance = null; +var currentScopeId = null; +function setCurrentRenderingInstance(instance) { + const prev = currentRenderingInstance; + currentRenderingInstance = instance; + currentScopeId = instance && instance.type.__scopeId || null; + return prev; +} +function pushScopeId(id) { + currentScopeId = id; +} +function popScopeId() { + currentScopeId = null; +} +var withScopeId = (_id) => withCtx; +function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) { + if (!ctx) return fn; + if (fn._n) { + return fn; + } + const renderFnWithContext = (...args) => { + if (renderFnWithContext._d) { + setBlockTracking(-1); + } + const prevInstance = setCurrentRenderingInstance(ctx); + let res; + try { + res = fn(...args); + } finally { + setCurrentRenderingInstance(prevInstance); + if (renderFnWithContext._d) { + setBlockTracking(1); + } + } + if (true) { + devtoolsComponentUpdated(ctx); + } + return res; + }; + renderFnWithContext._n = true; + renderFnWithContext._c = true; + renderFnWithContext._d = true; + return renderFnWithContext; +} +function validateDirectiveName(name) { + if (isBuiltInDirective(name)) { + warn$1("Do not use built-in directive ids as custom directive id: " + name); + } +} +function withDirectives(vnode, directives) { + if (currentRenderingInstance === null) { + warn$1(`withDirectives can only be used inside render functions.`); + return vnode; + } + const instance = getComponentPublicInstance(currentRenderingInstance); + const bindings = vnode.dirs || (vnode.dirs = []); + for (let i = 0; i < directives.length; i++) { + let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]; + if (dir) { + if (isFunction(dir)) { + dir = { + mounted: dir, + updated: dir + }; + } + if (dir.deep) { + traverse(value); + } + bindings.push({ + dir, + instance, + value, + oldValue: void 0, + arg, + modifiers + }); + } + } + return vnode; +} +function invokeDirectiveHook(vnode, prevVNode, instance, name) { + const bindings = vnode.dirs; + const oldBindings = prevVNode && prevVNode.dirs; + for (let i = 0; i < bindings.length; i++) { + const binding = bindings[i]; + if (oldBindings) { + binding.oldValue = oldBindings[i].value; + } + let hook = binding.dir[name]; + if (hook) { + pauseTracking(); + callWithAsyncErrorHandling(hook, instance, 8, [ + vnode.el, + binding, + vnode, + prevVNode + ]); + resetTracking(); + } + } +} +function provide(key, value) { + if (true) { + if (!currentInstance || currentInstance.isMounted) { + warn$1(`provide() can only be used inside setup().`); + } + } + if (currentInstance) { + let provides = currentInstance.provides; + const parentProvides = currentInstance.parent && currentInstance.parent.provides; + if (parentProvides === provides) { + provides = currentInstance.provides = Object.create(parentProvides); + } + provides[key] = value; + } +} +function inject(key, defaultValue, treatDefaultAsFactory = false) { + const instance = getCurrentInstance(); + if (instance || currentApp) { + let provides = currentApp ? currentApp._context.provides : instance ? instance.parent == null || instance.ce ? instance.vnode.appContext && instance.vnode.appContext.provides : instance.parent.provides : void 0; + if (provides && key in provides) { + return provides[key]; + } else if (arguments.length > 1) { + return treatDefaultAsFactory && isFunction(defaultValue) ? defaultValue.call(instance && instance.proxy) : defaultValue; + } else if (true) { + warn$1(`injection "${String(key)}" not found.`); + } + } else if (true) { + warn$1(`inject() can only be used inside setup() or functional components.`); + } +} +function hasInjectionContext() { + return !!(getCurrentInstance() || currentApp); +} +var ssrContextKey = Symbol.for("v-scx"); +var useSSRContext = () => { + { + const ctx = inject(ssrContextKey); + if (!ctx) { + warn$1( + `Server rendering context not provided. Make sure to only call useSSRContext() conditionally in the server build.` + ); + } + return ctx; + } +}; +function watchEffect(effect2, options) { + return doWatch(effect2, null, options); +} +function watchPostEffect(effect2, options) { + return doWatch( + effect2, + null, + true ? extend({}, options, { flush: "post" }) : { flush: "post" } + ); +} +function watchSyncEffect(effect2, options) { + return doWatch( + effect2, + null, + true ? extend({}, options, { flush: "sync" }) : { flush: "sync" } + ); +} +function watch2(source, cb, options) { + if (!isFunction(cb)) { + warn$1( + `\`watch(fn, options?)\` signature has been moved to a separate API. Use \`watchEffect(fn, options?)\` instead. \`watch\` now only supports \`watch(source, cb, options?) signature.` + ); + } + return doWatch(source, cb, options); +} +function doWatch(source, cb, options = EMPTY_OBJ) { + const { immediate, deep, flush, once } = options; + if (!cb) { + if (immediate !== void 0) { + warn$1( + `watch() "immediate" option is only respected when using the watch(source, callback, options?) signature.` + ); + } + if (deep !== void 0) { + warn$1( + `watch() "deep" option is only respected when using the watch(source, callback, options?) signature.` + ); + } + if (once !== void 0) { + warn$1( + `watch() "once" option is only respected when using the watch(source, callback, options?) signature.` + ); + } + } + const baseWatchOptions = extend({}, options); + if (true) baseWatchOptions.onWarn = warn$1; + const runsImmediately = cb && immediate || !cb && flush !== "post"; + let ssrCleanup; + if (isInSSRComponentSetup) { + if (flush === "sync") { + const ctx = useSSRContext(); + ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []); + } else if (!runsImmediately) { + const watchStopHandle = () => { + }; + watchStopHandle.stop = NOOP; + watchStopHandle.resume = NOOP; + watchStopHandle.pause = NOOP; + return watchStopHandle; + } + } + const instance = currentInstance; + baseWatchOptions.call = (fn, type, args) => callWithAsyncErrorHandling(fn, instance, type, args); + let isPre = false; + if (flush === "post") { + baseWatchOptions.scheduler = (job) => { + queuePostRenderEffect(job, instance && instance.suspense); + }; + } else if (flush !== "sync") { + isPre = true; + baseWatchOptions.scheduler = (job, isFirstRun) => { + if (isFirstRun) { + job(); + } else { + queueJob(job); + } + }; + } + baseWatchOptions.augmentJob = (job) => { + if (cb) { + job.flags |= 4; + } + if (isPre) { + job.flags |= 2; + if (instance) { + job.id = instance.uid; + job.i = instance; + } + } + }; + const watchHandle = watch(source, cb, baseWatchOptions); + if (isInSSRComponentSetup) { + if (ssrCleanup) { + ssrCleanup.push(watchHandle); + } else if (runsImmediately) { + watchHandle(); + } + } + return watchHandle; +} +function instanceWatch(source, value, options) { + const publicThis = this.proxy; + const getter = isString(source) ? source.includes(".") ? createPathGetter(publicThis, source) : () => publicThis[source] : source.bind(publicThis, publicThis); + let cb; + if (isFunction(value)) { + cb = value; + } else { + cb = value.handler; + options = value; + } + const reset = setCurrentInstance(this); + const res = doWatch(getter, cb.bind(publicThis), options); + reset(); + return res; +} +function createPathGetter(ctx, path) { + const segments = path.split("."); + return () => { + let cur = ctx; + for (let i = 0; i < segments.length && cur; i++) { + cur = cur[segments[i]]; + } + return cur; + }; +} +var pendingMounts = /* @__PURE__ */ new WeakMap(); +var TeleportEndKey = Symbol("_vte"); +var isTeleport = (type) => type.__isTeleport; +var isTeleportDisabled = (props) => props && (props.disabled || props.disabled === ""); +var isTeleportDeferred = (props) => props && (props.defer || props.defer === ""); +var isTargetSVG = (target) => typeof SVGElement !== "undefined" && target instanceof SVGElement; +var isTargetMathML = (target) => typeof MathMLElement === "function" && target instanceof MathMLElement; +var resolveTarget = (props, select) => { + const targetSelector = props && props.to; + if (isString(targetSelector)) { + if (!select) { + warn$1( + `Current renderer does not support string target for Teleports. (missing querySelector renderer option)` + ); + return null; + } else { + const target = select(targetSelector); + if (!target && !isTeleportDisabled(props)) { + warn$1( + `Failed to locate Teleport target with selector "${targetSelector}". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree.` + ); + } + return target; + } + } else { + if (!targetSelector && !isTeleportDisabled(props)) { + warn$1(`Invalid Teleport target: ${targetSelector}`); + } + return targetSelector; + } +}; +var TeleportImpl = { + name: "Teleport", + __isTeleport: true, + process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals) { + const { + mc: mountChildren, + pc: patchChildren, + pbc: patchBlockChildren, + o: { insert, querySelector, createText, createComment, parentNode } + } = internals; + const disabled = isTeleportDisabled(n2.props); + let { dynamicChildren } = n2; + if (isHmrUpdating) { + optimized = false; + dynamicChildren = null; + } + const mount = (vnode, container2, anchor2) => { + if (vnode.shapeFlag & 16) { + mountChildren( + vnode.children, + container2, + anchor2, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } + }; + const mountToTarget = (vnode = n2) => { + const disabled2 = isTeleportDisabled(vnode.props); + const target = vnode.target = resolveTarget(vnode.props, querySelector); + const targetAnchor = prepareAnchor(target, vnode, createText, insert); + if (target) { + if (namespace !== "svg" && isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace !== "mathml" && isTargetMathML(target)) { + namespace = "mathml"; + } + if (parentComponent && parentComponent.isCE) { + (parentComponent.ce._teleportTargets || (parentComponent.ce._teleportTargets = /* @__PURE__ */ new Set())).add(target); + } + if (!disabled2) { + mount(vnode, target, targetAnchor); + updateCssVars(vnode, false); + } + } else if (!disabled2) { + warn$1("Invalid Teleport target on mount:", target, `(${typeof target})`); + } + }; + const queuePendingMount = (vnode) => { + const mountJob = () => { + if (pendingMounts.get(vnode) !== mountJob) return; + pendingMounts.delete(vnode); + if (isTeleportDisabled(vnode.props)) { + const mountContainer = parentNode(vnode.el) || container; + mount(vnode, mountContainer, vnode.anchor); + updateCssVars(vnode, true); + } + mountToTarget(vnode); + }; + pendingMounts.set(vnode, mountJob); + queuePostRenderEffect(mountJob, parentSuspense); + }; + if (n1 == null) { + const placeholder = n2.el = true ? createComment("teleport start") : createText(""); + const mainAnchor = n2.anchor = true ? createComment("teleport end") : createText(""); + insert(placeholder, container, anchor); + insert(mainAnchor, container, anchor); + if (isTeleportDeferred(n2.props) || parentSuspense && parentSuspense.pendingBranch) { + queuePendingMount(n2); + return; + } + if (disabled) { + mount(n2, container, mainAnchor); + updateCssVars(n2, true); + } + mountToTarget(); + } else { + n2.el = n1.el; + const mainAnchor = n2.anchor = n1.anchor; + const pendingMount = pendingMounts.get(n1); + if (pendingMount) { + pendingMount.flags |= 8; + pendingMounts.delete(n1); + queuePendingMount(n2); + return; + } + n2.targetStart = n1.targetStart; + const target = n2.target = n1.target; + const targetAnchor = n2.targetAnchor = n1.targetAnchor; + const wasDisabled = isTeleportDisabled(n1.props); + const currentContainer = wasDisabled ? container : target; + const currentAnchor = wasDisabled ? mainAnchor : targetAnchor; + if (namespace === "svg" || isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace === "mathml" || isTargetMathML(target)) { + namespace = "mathml"; + } + if (dynamicChildren) { + patchBlockChildren( + n1.dynamicChildren, + dynamicChildren, + currentContainer, + parentComponent, + parentSuspense, + namespace, + slotScopeIds + ); + traverseStaticChildren(n1, n2, false); + } else if (!optimized) { + patchChildren( + n1, + n2, + currentContainer, + currentAnchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + false + ); + } + if (disabled) { + if (!wasDisabled) { + moveTeleport( + n2, + container, + mainAnchor, + internals, + 1 + ); + } else { + if (n2.props && n1.props && n2.props.to !== n1.props.to) { + n2.props.to = n1.props.to; + } + } + } else { + if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) { + const nextTarget = n2.target = resolveTarget( + n2.props, + querySelector + ); + if (nextTarget) { + moveTeleport( + n2, + nextTarget, + null, + internals, + 0 + ); + } else if (true) { + warn$1( + "Invalid Teleport target on update:", + target, + `(${typeof target})` + ); + } + } else if (wasDisabled) { + moveTeleport( + n2, + target, + targetAnchor, + internals, + 1 + ); + } + } + updateCssVars(n2, disabled); + } + }, + remove(vnode, parentComponent, parentSuspense, { um: unmount, o: { remove: hostRemove } }, doRemove) { + const { + shapeFlag, + children, + anchor, + targetStart, + targetAnchor, + target, + props + } = vnode; + let shouldRemove = doRemove || !isTeleportDisabled(props); + const pendingMount = pendingMounts.get(vnode); + if (pendingMount) { + pendingMount.flags |= 8; + pendingMounts.delete(vnode); + shouldRemove = false; + } + if (target) { + hostRemove(targetStart); + hostRemove(targetAnchor); + } + doRemove && hostRemove(anchor); + if (shapeFlag & 16) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + unmount( + child, + parentComponent, + parentSuspense, + shouldRemove, + !!child.dynamicChildren + ); + } + } + }, + move: moveTeleport, + hydrate: hydrateTeleport +}; +function moveTeleport(vnode, container, parentAnchor, { o: { insert }, m: move }, moveType = 2) { + if (moveType === 0) { + insert(vnode.targetAnchor, container, parentAnchor); + } + const { el, anchor, shapeFlag, children, props } = vnode; + const isReorder = moveType === 2; + if (isReorder) { + insert(el, container, parentAnchor); + } + if (!pendingMounts.has(vnode) && (!isReorder || isTeleportDisabled(props))) { + if (shapeFlag & 16) { + for (let i = 0; i < children.length; i++) { + move( + children[i], + container, + parentAnchor, + 2 + ); + } + } + } + if (isReorder) { + insert(anchor, container, parentAnchor); + } +} +function hydrateTeleport(node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized, { + o: { nextSibling, parentNode, querySelector, insert, createText } +}, hydrateChildren) { + function hydrateAnchor(target2, targetNode) { + let targetAnchor = targetNode; + while (targetAnchor) { + if (targetAnchor && targetAnchor.nodeType === 8) { + if (targetAnchor.data === "teleport start anchor") { + vnode.targetStart = targetAnchor; + } else if (targetAnchor.data === "teleport anchor") { + vnode.targetAnchor = targetAnchor; + target2._lpa = vnode.targetAnchor && nextSibling(vnode.targetAnchor); + break; + } + } + targetAnchor = nextSibling(targetAnchor); + } + } + function hydrateDisabledTeleport(node2, vnode2) { + vnode2.anchor = hydrateChildren( + nextSibling(node2), + vnode2, + parentNode(node2), + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + const target = vnode.target = resolveTarget( + vnode.props, + querySelector + ); + const disabled = isTeleportDisabled(vnode.props); + if (target) { + const targetNode = target._lpa || target.firstChild; + if (vnode.shapeFlag & 16) { + if (disabled) { + hydrateDisabledTeleport(node, vnode); + hydrateAnchor(target, targetNode); + if (!vnode.targetAnchor) { + prepareAnchor( + target, + vnode, + createText, + insert, + // if target is the same as the main view, insert anchors before current node + // to avoid hydrating mismatch + parentNode(node) === target ? node : null + ); + } + } else { + vnode.anchor = nextSibling(node); + hydrateAnchor(target, targetNode); + if (!vnode.targetAnchor) { + prepareAnchor(target, vnode, createText, insert); + } + hydrateChildren( + targetNode && nextSibling(targetNode), + vnode, + target, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } + updateCssVars(vnode, disabled); + } else if (disabled) { + if (vnode.shapeFlag & 16) { + hydrateDisabledTeleport(node, vnode); + vnode.targetStart = node; + vnode.targetAnchor = nextSibling(node); + } + } + return vnode.anchor && nextSibling(vnode.anchor); +} +var Teleport = TeleportImpl; +function updateCssVars(vnode, isDisabled) { + const ctx = vnode.ctx; + if (ctx && ctx.ut) { + let node, anchor; + if (isDisabled) { + node = vnode.el; + anchor = vnode.anchor; + } else { + node = vnode.targetStart; + anchor = vnode.targetAnchor; + } + while (node && node !== anchor) { + if (node.nodeType === 1) node.setAttribute("data-v-owner", ctx.uid); + node = node.nextSibling; + } + ctx.ut(); + } +} +function prepareAnchor(target, vnode, createText, insert, anchor = null) { + const targetStart = vnode.targetStart = createText(""); + const targetAnchor = vnode.targetAnchor = createText(""); + targetStart[TeleportEndKey] = targetAnchor; + if (target) { + insert(targetStart, target, anchor); + insert(targetAnchor, target, anchor); + } + return targetAnchor; +} +var leaveCbKey = Symbol("_leaveCb"); +var enterCbKey = Symbol("_enterCb"); +function useTransitionState() { + const state = { + isMounted: false, + isLeaving: false, + isUnmounting: false, + leavingVNodes: /* @__PURE__ */ new Map() + }; + onMounted(() => { + state.isMounted = true; + }); + onBeforeUnmount(() => { + state.isUnmounting = true; + }); + return state; +} +var TransitionHookValidator = [Function, Array]; +var BaseTransitionPropsValidators = { + mode: String, + appear: Boolean, + persisted: Boolean, + // enter + onBeforeEnter: TransitionHookValidator, + onEnter: TransitionHookValidator, + onAfterEnter: TransitionHookValidator, + onEnterCancelled: TransitionHookValidator, + // leave + onBeforeLeave: TransitionHookValidator, + onLeave: TransitionHookValidator, + onAfterLeave: TransitionHookValidator, + onLeaveCancelled: TransitionHookValidator, + // appear + onBeforeAppear: TransitionHookValidator, + onAppear: TransitionHookValidator, + onAfterAppear: TransitionHookValidator, + onAppearCancelled: TransitionHookValidator +}; +var recursiveGetSubtree = (instance) => { + const subTree = instance.subTree; + return subTree.component ? recursiveGetSubtree(subTree.component) : subTree; +}; +var BaseTransitionImpl = { + name: `BaseTransition`, + props: BaseTransitionPropsValidators, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const state = useTransitionState(); + return () => { + const children = slots.default && getTransitionRawChildren(slots.default(), true); + const child = children && children.length ? findNonCommentChild(children) : ( + // Keep explicit default-slot conditionals on the same transition path + // as regular v-if branches, which render a comment placeholder. + instance.subTree ? createCommentVNode() : void 0 + ); + if (!child) { + return; + } + const rawProps = toRaw(props); + const { mode } = rawProps; + if (mode && mode !== "in-out" && mode !== "out-in" && mode !== "default") { + warn$1(`invalid mode: ${mode}`); + } + if (state.isLeaving) { + return emptyPlaceholder(child); + } + const innerChild = getInnerChild$1(child); + if (!innerChild) { + return emptyPlaceholder(child); + } + let enterHooks = resolveTransitionHooks( + innerChild, + rawProps, + state, + instance, + // #11061, ensure enterHooks is fresh after clone + (hooks) => enterHooks = hooks + ); + if (innerChild.type !== Comment) { + setTransitionHooks(innerChild, enterHooks); + } + let oldInnerChild = instance.subTree && getInnerChild$1(instance.subTree); + if (oldInnerChild && oldInnerChild.type !== Comment && !isSameVNodeType(oldInnerChild, innerChild) && recursiveGetSubtree(instance).type !== Comment) { + let leavingHooks = resolveTransitionHooks( + oldInnerChild, + rawProps, + state, + instance + ); + setTransitionHooks(oldInnerChild, leavingHooks); + if (mode === "out-in" && innerChild.type !== Comment) { + state.isLeaving = true; + leavingHooks.afterLeave = () => { + state.isLeaving = false; + if (!(instance.job.flags & 8)) { + instance.update(); + } + delete leavingHooks.afterLeave; + oldInnerChild = void 0; + }; + return emptyPlaceholder(child); + } else if (mode === "in-out" && innerChild.type !== Comment) { + leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => { + const leavingVNodesCache = getLeavingNodesForType( + state, + oldInnerChild + ); + leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild; + el[leaveCbKey] = () => { + earlyRemove(); + el[leaveCbKey] = void 0; + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + enterHooks.delayedLeave = () => { + delayedLeave(); + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + }; + } else { + oldInnerChild = void 0; + } + } else if (oldInnerChild) { + oldInnerChild = void 0; + } + return child; + }; + } +}; +function findNonCommentChild(children) { + let child = children[0]; + if (children.length > 1) { + let hasFound = false; + for (const c of children) { + if (c.type !== Comment) { + if (hasFound) { + warn$1( + " can only be used on a single element or component. Use for lists." + ); + break; + } + child = c; + hasFound = true; + if (false) break; + } + } + } + return child; +} +var BaseTransition = BaseTransitionImpl; +function getLeavingNodesForType(state, vnode) { + const { leavingVNodes } = state; + let leavingVNodesCache = leavingVNodes.get(vnode.type); + if (!leavingVNodesCache) { + leavingVNodesCache = /* @__PURE__ */ Object.create(null); + leavingVNodes.set(vnode.type, leavingVNodesCache); + } + return leavingVNodesCache; +} +function resolveTransitionHooks(vnode, props, state, instance, postClone) { + const { + appear, + mode, + persisted = false, + onBeforeEnter, + onEnter, + onAfterEnter, + onEnterCancelled, + onBeforeLeave, + onLeave, + onAfterLeave, + onLeaveCancelled, + onBeforeAppear, + onAppear, + onAfterAppear, + onAppearCancelled + } = props; + const key = String(vnode.key); + const leavingVNodesCache = getLeavingNodesForType(state, vnode); + const callHook3 = (hook, args) => { + hook && callWithAsyncErrorHandling( + hook, + instance, + 9, + args + ); + }; + const callAsyncHook = (hook, args) => { + const done = args[1]; + callHook3(hook, args); + if (isArray(hook)) { + if (hook.every((hook2) => hook2.length <= 1)) done(); + } else if (hook.length <= 1) { + done(); + } + }; + const hooks = { + mode, + persisted, + beforeEnter(el) { + let hook = onBeforeEnter; + if (!state.isMounted) { + if (appear) { + hook = onBeforeAppear || onBeforeEnter; + } else { + return; + } + } + if (el[leaveCbKey]) { + el[leaveCbKey]( + true + /* cancelled */ + ); + } + const leavingVNode = leavingVNodesCache[key]; + if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el[leaveCbKey]) { + leavingVNode.el[leaveCbKey](); + } + callHook3(hook, [el]); + }, + enter(el) { + if (!isHmrUpdating && leavingVNodesCache[key] === vnode) return; + let hook = onEnter; + let afterHook = onAfterEnter; + let cancelHook = onEnterCancelled; + if (!state.isMounted) { + if (appear) { + hook = onAppear || onEnter; + afterHook = onAfterAppear || onAfterEnter; + cancelHook = onAppearCancelled || onEnterCancelled; + } else { + return; + } + } + let called = false; + el[enterCbKey] = (cancelled) => { + if (called) return; + called = true; + if (cancelled) { + callHook3(cancelHook, [el]); + } else { + callHook3(afterHook, [el]); + } + if (hooks.delayedLeave) { + hooks.delayedLeave(); + } + el[enterCbKey] = void 0; + }; + const done = el[enterCbKey].bind(null, false); + if (hook) { + callAsyncHook(hook, [el, done]); + } else { + done(); + } + }, + leave(el, remove2) { + const key2 = String(vnode.key); + if (el[enterCbKey]) { + el[enterCbKey]( + true + /* cancelled */ + ); + } + if (state.isUnmounting) { + return remove2(); + } + callHook3(onBeforeLeave, [el]); + let called = false; + el[leaveCbKey] = (cancelled) => { + if (called) return; + called = true; + remove2(); + if (cancelled) { + callHook3(onLeaveCancelled, [el]); + } else { + callHook3(onAfterLeave, [el]); + } + el[leaveCbKey] = void 0; + if (leavingVNodesCache[key2] === vnode) { + delete leavingVNodesCache[key2]; + } + }; + const done = el[leaveCbKey].bind(null, false); + leavingVNodesCache[key2] = vnode; + if (onLeave) { + callAsyncHook(onLeave, [el, done]); + } else { + done(); + } + }, + clone(vnode2) { + const hooks2 = resolveTransitionHooks( + vnode2, + props, + state, + instance, + postClone + ); + if (postClone) postClone(hooks2); + return hooks2; + } + }; + return hooks; +} +function emptyPlaceholder(vnode) { + if (isKeepAlive(vnode)) { + vnode = cloneVNode(vnode); + vnode.children = null; + return vnode; + } +} +function getInnerChild$1(vnode) { + if (!isKeepAlive(vnode)) { + if (isTeleport(vnode.type) && vnode.children) { + return findNonCommentChild(vnode.children); + } + return vnode; + } + if (vnode.component) { + return vnode.component.subTree; + } + const { shapeFlag, children } = vnode; + if (children) { + if (shapeFlag & 16) { + return children[0]; + } + if (shapeFlag & 32 && isFunction(children.default)) { + return children.default(); + } + } +} +function setTransitionHooks(vnode, hooks) { + if (vnode.shapeFlag & 6 && vnode.component) { + vnode.transition = hooks; + setTransitionHooks(vnode.component.subTree, hooks); + } else if (vnode.shapeFlag & 128) { + vnode.ssContent.transition = hooks.clone(vnode.ssContent); + vnode.ssFallback.transition = hooks.clone(vnode.ssFallback); + } else { + vnode.transition = hooks; + } +} +function getTransitionRawChildren(children, keepComment = false, parentKey) { + let ret = []; + let keyedFragmentCount = 0; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + const key = parentKey == null ? child.key : String(parentKey) + String(child.key != null ? child.key : i); + if (child.type === Fragment) { + if (child.patchFlag & 128) keyedFragmentCount++; + ret = ret.concat( + getTransitionRawChildren(child.children, keepComment, key) + ); + } else if (keepComment || child.type !== Comment) { + ret.push(key != null ? cloneVNode(child, { key }) : child); + } + } + if (keyedFragmentCount > 1) { + for (let i = 0; i < ret.length; i++) { + ret[i].patchFlag = -2; + } + } + return ret; +} +function defineComponent(options, extraOptions) { + return isFunction(options) ? ( + // #8236: extend call and options.name access are considered side-effects + // by Rollup, so we have to wrap it in a pure-annotated IIFE. + (() => extend({ name: options.name }, extraOptions, { setup: options }))() + ) : options; +} +function useId() { + const i = getCurrentInstance(); + if (i) { + return (i.appContext.config.idPrefix || "v") + "-" + i.ids[0] + i.ids[1]++; + } else if (true) { + warn$1( + `useId() is called when there is no active component instance to be associated with.` + ); + } + return ""; +} +function markAsyncBoundary(instance) { + instance.ids = [instance.ids[0] + instance.ids[2]++ + "-", 0, 0]; +} +var knownTemplateRefs = /* @__PURE__ */ new WeakSet(); +function useTemplateRef(key) { + const i = getCurrentInstance(); + const r = shallowRef(null); + if (i) { + const refs = i.refs === EMPTY_OBJ ? i.refs = {} : i.refs; + if (isTemplateRefKey(refs, key)) { + warn$1(`useTemplateRef('${key}') already exists.`); + } else { + Object.defineProperty(refs, key, { + enumerable: true, + get: () => r.value, + set: (val) => r.value = val + }); + } + } else if (true) { + warn$1( + `useTemplateRef() is called when there is no active component instance to be associated with.` + ); + } + const ret = true ? readonly(r) : r; + if (true) { + knownTemplateRefs.add(ret); + } + return ret; +} +function isTemplateRefKey(refs, key) { + let desc; + return !!((desc = Object.getOwnPropertyDescriptor(refs, key)) && !desc.configurable); +} +var pendingSetRefMap = /* @__PURE__ */ new WeakMap(); +function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = false) { + if (isArray(rawRef)) { + rawRef.forEach( + (r, i) => setRef( + r, + oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), + parentSuspense, + vnode, + isUnmount + ) + ); + return; + } + if (isAsyncWrapper(vnode) && !isUnmount) { + if (vnode.shapeFlag & 512 && vnode.type.__asyncResolved && vnode.component.subTree.component) { + setRef(rawRef, oldRawRef, parentSuspense, vnode.component.subTree); + } + return; + } + const refValue = vnode.shapeFlag & 4 ? getComponentPublicInstance(vnode.component) : vnode.el; + const value = isUnmount ? null : refValue; + const { i: owner, r: ref2 } = rawRef; + if (!owner) { + warn$1( + `Missing ref owner context. ref cannot be used on hoisted vnodes. A vnode with ref must be created inside the render function.` + ); + return; + } + const oldRef = oldRawRef && oldRawRef.r; + const refs = owner.refs === EMPTY_OBJ ? owner.refs = {} : owner.refs; + const setupState = owner.setupState; + const rawSetupState = toRaw(setupState); + const canSetSetupRef = setupState === EMPTY_OBJ ? NO : (key) => { + if (true) { + if (hasOwn(rawSetupState, key) && !isRef2(rawSetupState[key])) { + warn$1( + `Template ref "${key}" used on a non-ref value. It will not work in the production build.` + ); + } + if (knownTemplateRefs.has(rawSetupState[key])) { + return false; + } + } + if (isTemplateRefKey(refs, key)) { + return false; + } + return hasOwn(rawSetupState, key); + }; + const canSetRef = (ref22, key) => { + if (knownTemplateRefs.has(ref22)) { + return false; + } + if (key && isTemplateRefKey(refs, key)) { + return false; + } + return true; + }; + if (oldRef != null && oldRef !== ref2) { + invalidatePendingSetRef(oldRawRef); + if (isString(oldRef)) { + refs[oldRef] = null; + if (canSetSetupRef(oldRef)) { + setupState[oldRef] = null; + } + } else if (isRef2(oldRef)) { + const oldRawRefAtom = oldRawRef; + if (canSetRef(oldRef, oldRawRefAtom.k)) { + oldRef.value = null; + } + if (oldRawRefAtom.k) refs[oldRawRefAtom.k] = null; + } + } + if (isFunction(ref2)) { + callWithErrorHandling(ref2, owner, 12, [value, refs]); + } else { + const _isString = isString(ref2); + const _isRef = isRef2(ref2); + if (_isString || _isRef) { + const doSet = () => { + if (rawRef.f) { + const existing = _isString ? canSetSetupRef(ref2) ? setupState[ref2] : refs[ref2] : canSetRef(ref2) || !rawRef.k ? ref2.value : refs[rawRef.k]; + if (isUnmount) { + isArray(existing) && remove(existing, refValue); + } else { + if (!isArray(existing)) { + if (_isString) { + refs[ref2] = [refValue]; + if (canSetSetupRef(ref2)) { + setupState[ref2] = refs[ref2]; + } + } else { + const newVal = [refValue]; + if (canSetRef(ref2, rawRef.k)) { + ref2.value = newVal; + } + if (rawRef.k) refs[rawRef.k] = newVal; + } + } else if (!existing.includes(refValue)) { + existing.push(refValue); + } + } + } else if (_isString) { + refs[ref2] = value; + if (canSetSetupRef(ref2)) { + setupState[ref2] = value; + } + } else if (_isRef) { + if (canSetRef(ref2, rawRef.k)) { + ref2.value = value; + } + if (rawRef.k) refs[rawRef.k] = value; + } else if (true) { + warn$1("Invalid template ref type:", ref2, `(${typeof ref2})`); + } + }; + if (value) { + const job = () => { + doSet(); + pendingSetRefMap.delete(rawRef); + }; + job.id = -1; + pendingSetRefMap.set(rawRef, job); + queuePostRenderEffect(job, parentSuspense); + } else { + invalidatePendingSetRef(rawRef); + doSet(); + } + } else if (true) { + warn$1("Invalid template ref type:", ref2, `(${typeof ref2})`); + } + } +} +function invalidatePendingSetRef(rawRef) { + const pendingSetRef = pendingSetRefMap.get(rawRef); + if (pendingSetRef) { + pendingSetRef.flags |= 8; + pendingSetRefMap.delete(rawRef); + } +} +var hasLoggedMismatchError = false; +var logMismatchError = () => { + if (hasLoggedMismatchError) { + return; + } + console.error("Hydration completed but contains mismatches."); + hasLoggedMismatchError = true; +}; +var isSVGContainer = (container) => container.namespaceURI.includes("svg") && container.tagName !== "foreignObject"; +var isMathMLContainer = (container) => container.namespaceURI.includes("MathML"); +var getContainerType = (container) => { + if (container.nodeType !== 1) return void 0; + if (isSVGContainer(container)) return "svg"; + if (isMathMLContainer(container)) return "mathml"; + return void 0; +}; +var isComment = (node) => node.nodeType === 8; +function createHydrationFunctions(rendererInternals) { + const { + mt: mountComponent, + p: patch, + o: { + patchProp: patchProp2, + createText, + nextSibling, + parentNode, + remove: remove2, + insert, + createComment + } + } = rendererInternals; + const hydrate2 = (vnode, container) => { + if (!container.hasChildNodes()) { + warn$1( + `Attempting to hydrate existing markup but container is empty. Performing full mount instead.` + ); + patch(null, vnode, container); + flushPostFlushCbs(); + container._vnode = vnode; + return; + } + hydrateNode(container.firstChild, vnode, null, null, null); + flushPostFlushCbs(); + container._vnode = vnode; + }; + const hydrateNode = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized = false) => { + optimized = optimized || !!vnode.dynamicChildren; + const isFragmentStart = isComment(node) && node.data === "["; + const onMismatch = () => handleMismatch( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + isFragmentStart + ); + const { type, ref: ref2, shapeFlag, patchFlag } = vnode; + let domType = node.nodeType; + vnode.el = node; + if (true) { + def(node, "__vnode", vnode, true); + def(node, "__vueParentComponent", parentComponent, true); + } + if (patchFlag === -2) { + optimized = false; + vnode.dynamicChildren = null; + } + let nextNode = null; + switch (type) { + case Text: + if (domType !== 3) { + if (vnode.children === "") { + insert(vnode.el = createText(""), parentNode(node), node); + nextNode = node; + } else { + nextNode = onMismatch(); + } + } else { + if (node.data !== vnode.children) { + warn$1( + `Hydration text mismatch in`, + node.parentNode, + ` + - rendered on server: ${JSON.stringify( + node.data + )} + - expected on client: ${JSON.stringify(vnode.children)}` + ); + logMismatchError(); + node.data = vnode.children; + } + nextNode = nextSibling(node); + } + break; + case Comment: + if (isTemplateNode(node)) { + nextNode = nextSibling(node); + replaceNode( + vnode.el = node.content.firstChild, + node, + parentComponent + ); + } else if (domType !== 8 || isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = nextSibling(node); + } + break; + case Static: + if (isFragmentStart) { + node = nextSibling(node); + domType = node.nodeType; + } + if (domType === 1 || domType === 3) { + nextNode = node; + const needToAdoptContent = !vnode.children.length; + for (let i = 0; i < vnode.staticCount; i++) { + if (needToAdoptContent) + vnode.children += nextNode.nodeType === 1 ? nextNode.outerHTML : nextNode.data; + if (i === vnode.staticCount - 1) { + vnode.anchor = nextNode; + } + nextNode = nextSibling(nextNode); + } + return isFragmentStart ? nextSibling(nextNode) : nextNode; + } else { + onMismatch(); + } + break; + case Fragment: + if (!isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = hydrateFragment( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + break; + default: + if (shapeFlag & 1) { + if ((domType !== 1 || vnode.type.toLowerCase() !== node.tagName.toLowerCase()) && !isTemplateNode(node)) { + nextNode = onMismatch(); + } else { + nextNode = hydrateElement( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } else if (shapeFlag & 6) { + vnode.slotScopeIds = slotScopeIds; + const container = parentNode(node); + if (isFragmentStart) { + nextNode = locateClosingAnchor(node); + } else if (isComment(node) && node.data === "teleport start") { + nextNode = locateClosingAnchor(node, node.data, "teleport end"); + } else { + nextNode = nextSibling(node); + } + mountComponent( + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + optimized + ); + if (isAsyncWrapper(vnode) && !vnode.type.__asyncResolved) { + let subTree; + if (isFragmentStart) { + subTree = createVNode(Fragment); + subTree.anchor = nextNode ? nextNode.previousSibling : container.lastChild; + } else { + subTree = node.nodeType === 3 ? createTextVNode("") : createVNode("div"); + } + subTree.el = node; + vnode.component.subTree = subTree; + } + } else if (shapeFlag & 64) { + if (domType !== 8) { + nextNode = onMismatch(); + } else { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized, + rendererInternals, + hydrateChildren + ); + } + } else if (shapeFlag & 128) { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + getContainerType(parentNode(node)), + slotScopeIds, + optimized, + rendererInternals, + hydrateNode + ); + } else if (true) { + warn$1("Invalid HostVNode type:", type, `(${typeof type})`); + } + } + if (ref2 != null) { + setRef(ref2, null, parentSuspense, vnode); + } + return nextNode; + }; + const hydrateElement = (el, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!vnode.dynamicChildren; + const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode; + const forcePatch = type === "input" || type === "option"; + if (true) { + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "created"); + } + let needCallTransitionHooks = false; + if (isTemplateNode(el)) { + needCallTransitionHooks = needTransition( + null, + // no need check parentSuspense in hydration + transition + ) && parentComponent && parentComponent.vnode.props && parentComponent.vnode.props.appear; + const content = el.content.firstChild; + if (needCallTransitionHooks) { + const cls = content.getAttribute("class"); + if (cls) content.$cls = cls; + transition.beforeEnter(content); + } + replaceNode(content, el, parentComponent); + vnode.el = el = content; + } + if (shapeFlag & 16 && // skip if element has innerHTML / textContent + !(props && (props.innerHTML || props.textContent))) { + let next = hydrateChildren( + el.firstChild, + vnode, + el, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + let hasWarned2 = false; + while (next) { + if (!isMismatchAllowed( + el, + 1 + /* CHILDREN */ + )) { + if (!hasWarned2) { + warn$1( + `Hydration children mismatch on`, + el, + ` +Server rendered element contains more child nodes than client vdom.` + ); + hasWarned2 = true; + } + logMismatchError(); + } + const cur = next; + next = next.nextSibling; + remove2(cur); + } + } else if (shapeFlag & 8) { + let clientText = vnode.children; + if (clientText[0] === "\n" && (el.tagName === "PRE" || el.tagName === "TEXTAREA")) { + clientText = clientText.slice(1); + } + const { textContent } = el; + if (textContent !== clientText && // innerHTML normalize \r\n or \r into a single \n in the DOM + textContent !== clientText.replace(/\r\n|\r/g, "\n")) { + if (!isMismatchAllowed( + el, + 0 + /* TEXT */ + )) { + warn$1( + `Hydration text content mismatch on`, + el, + ` + - rendered on server: ${textContent} + - expected on client: ${clientText}` + ); + logMismatchError(); + } + el.textContent = vnode.children; + } + } + if (props) { + if (true) { + const isCustomElement = el.tagName.includes("-"); + for (const key in props) { + if (// #11189 skip if this node has directives that have created hooks + // as it could have mutated the DOM in any possible way + !(dirs && dirs.some((d) => d.dir.created)) && propHasMismatch(el, key, props[key], vnode, parentComponent)) { + logMismatchError(); + } + if (forcePatch && (key.endsWith("value") || key === "indeterminate") || isOn(key) && !isReservedProp(key) || // force hydrate v-bind with .prop modifiers + key[0] === "." || isCustomElement && !isReservedProp(key)) { + patchProp2(el, key, null, props[key], void 0, parentComponent); + } + } + } else if (props.onClick) { + patchProp2( + el, + "onClick", + null, + props.onClick, + void 0, + parentComponent + ); + } else if (patchFlag & 4 && isReactive(props.style)) { + for (const key in props.style) props.style[key]; + } + } + let vnodeHooks; + if (vnodeHooks = props && props.onVnodeBeforeMount) { + invokeVNodeHook(vnodeHooks, parentComponent, vnode); + } + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "beforeMount"); + } + if ((vnodeHooks = props && props.onVnodeMounted) || dirs || needCallTransitionHooks) { + queueEffectWithSuspense(() => { + vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode); + needCallTransitionHooks && transition.enter(el); + dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted"); + }, parentSuspense); + } + } + return el.nextSibling; + }; + const hydrateChildren = (node, parentVNode, container, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!parentVNode.dynamicChildren; + const children = parentVNode.children; + const l = children.length; + let hasWarned2 = false; + for (let i = 0; i < l; i++) { + const vnode = optimized ? children[i] : children[i] = normalizeVNode(children[i]); + const isText = vnode.type === Text; + if (node) { + if (isText && !optimized) { + if (i + 1 < l && normalizeVNode(children[i + 1]).type === Text) { + insert( + createText( + node.data.slice(vnode.children.length) + ), + container, + nextSibling(node) + ); + node.data = vnode.children; + } + } + node = hydrateNode( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } else if (isText && !vnode.children) { + insert(vnode.el = createText(""), container); + } else { + if (!isMismatchAllowed( + container, + 1 + /* CHILDREN */ + )) { + if (!hasWarned2) { + warn$1( + `Hydration children mismatch on`, + container, + ` +Server rendered element contains fewer child nodes than client vdom.` + ); + hasWarned2 = true; + } + logMismatchError(); + } + patch( + null, + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + } + } + return node; + }; + const hydrateFragment = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + const { slotScopeIds: fragmentSlotScopeIds } = vnode; + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds; + } + const container = parentNode(node); + const next = hydrateChildren( + nextSibling(node), + vnode, + container, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + if (next && isComment(next) && next.data === "]") { + return nextSibling(vnode.anchor = next); + } else { + logMismatchError(); + insert(vnode.anchor = createComment(`]`), container, next); + return next; + } + }; + const handleMismatch = (node, vnode, parentComponent, parentSuspense, slotScopeIds, isFragment) => { + if (!isMismatchAllowed( + node.parentElement, + 1 + /* CHILDREN */ + )) { + warn$1( + `Hydration node mismatch: +- rendered on server:`, + node, + node.nodeType === 3 ? `(text)` : isComment(node) && node.data === "[" ? `(start of fragment)` : ``, + ` +- expected on client:`, + vnode.type + ); + logMismatchError(); + } + vnode.el = null; + if (isFragment) { + const end = locateClosingAnchor(node); + while (true) { + const next2 = nextSibling(node); + if (next2 && next2 !== end) { + remove2(next2); + } else { + break; + } + } + } + const next = nextSibling(node); + const container = parentNode(node); + remove2(node); + patch( + null, + vnode, + container, + next, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + if (parentComponent) { + parentComponent.vnode.el = vnode.el; + updateHOCHostEl(parentComponent, vnode.el); + } + return next; + }; + const locateClosingAnchor = (node, open = "[", close = "]") => { + let match = 0; + while (node) { + node = nextSibling(node); + if (node && isComment(node)) { + if (node.data === open) match++; + if (node.data === close) { + if (match === 0) { + return nextSibling(node); + } else { + match--; + } + } + } + } + return node; + }; + const replaceNode = (newNode, oldNode, parentComponent) => { + const parentNode2 = oldNode.parentNode; + if (parentNode2) { + parentNode2.replaceChild(newNode, oldNode); + } + let parent = parentComponent; + while (parent) { + if (parent.vnode.el === oldNode) { + parent.vnode.el = parent.subTree.el = newNode; + } + parent = parent.parent; + } + }; + const isTemplateNode = (node) => { + return node.nodeType === 1 && node.tagName === "TEMPLATE"; + }; + return [hydrate2, hydrateNode]; +} +function propHasMismatch(el, key, clientValue, vnode, instance) { + let mismatchType; + let mismatchKey; + let actual; + let expected; + if (key === "class") { + if (el.$cls) { + actual = el.$cls; + delete el.$cls; + } else { + actual = el.getAttribute("class"); + } + expected = normalizeClass(clientValue); + if (!isSetEqual(toClassSet(actual || ""), toClassSet(expected))) { + mismatchType = 2; + mismatchKey = `class`; + } + } else if (key === "style") { + actual = el.getAttribute("style") || ""; + expected = isString(clientValue) ? clientValue : stringifyStyle(normalizeStyle(clientValue)); + const actualMap = toStyleMap(actual); + const expectedMap = toStyleMap(expected); + if (vnode.dirs) { + for (const { dir, value } of vnode.dirs) { + if (dir.name === "show" && !value) { + expectedMap.set("display", "none"); + } + } + } + if (instance) { + resolveCssVars(instance, vnode, expectedMap); + } + if (!isMapEqual(actualMap, expectedMap)) { + mismatchType = 3; + mismatchKey = "style"; + } + } else if (el instanceof SVGElement && isKnownSvgAttr(key) || el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) { + if (isBooleanAttr(key)) { + actual = el.hasAttribute(key); + expected = includeBooleanAttr(clientValue); + } else if (clientValue == null) { + actual = el.hasAttribute(key); + expected = false; + } else { + if (el.hasAttribute(key)) { + actual = el.getAttribute(key); + } else if (key === "value" && el.tagName === "TEXTAREA") { + actual = el.value; + } else { + actual = false; + } + expected = isRenderableAttrValue(clientValue) ? String(clientValue) : false; + } + if (actual !== expected) { + mismatchType = 4; + mismatchKey = key; + } + } + if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) { + const format = (v) => v === false ? `(not rendered)` : `${mismatchKey}="${v}"`; + const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`; + const postSegment = ` + - rendered on server: ${format(actual)} + - expected on client: ${format(expected)} + Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead. + You should fix the source of the mismatch.`; + { + warn$1(preSegment, el, postSegment); + } + return true; + } + return false; +} +function toClassSet(str) { + return new Set(str.trim().split(/\s+/)); +} +function isSetEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const s of a) { + if (!b.has(s)) { + return false; + } + } + return true; +} +function toStyleMap(str) { + const styleMap = /* @__PURE__ */ new Map(); + for (const item of str.split(";")) { + let [key, value] = item.split(":"); + key = key.trim(); + value = value && value.trim(); + if (key && value) { + styleMap.set(key, value); + } + } + return styleMap; +} +function isMapEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const [key, value] of a) { + if (value !== b.get(key)) { + return false; + } + } + return true; +} +function resolveCssVars(instance, vnode, expectedMap) { + const root = instance.subTree; + if (instance.getCssVars && (vnode === root || root && root.type === Fragment && root.children.includes(vnode))) { + const cssVars = instance.getCssVars(); + for (const key in cssVars) { + const value = normalizeCssVarValue(cssVars[key]); + expectedMap.set(`--${getEscapedCssVarName(key, false)}`, value); + } + } + if (vnode === root && instance.parent) { + resolveCssVars(instance.parent, instance.vnode, expectedMap); + } +} +var allowMismatchAttr = "data-allow-mismatch"; +var MismatchTypeString = { + [ + 0 + /* TEXT */ + ]: "text", + [ + 1 + /* CHILDREN */ + ]: "children", + [ + 2 + /* CLASS */ + ]: "class", + [ + 3 + /* STYLE */ + ]: "style", + [ + 4 + /* ATTRIBUTE */ + ]: "attribute" +}; +function isMismatchAllowed(el, allowedType) { + if (allowedType === 0 || allowedType === 1) { + while (el && !el.hasAttribute(allowMismatchAttr)) { + el = el.parentElement; + } + } + const allowedAttr = el && el.getAttribute(allowMismatchAttr); + if (allowedAttr == null) { + return false; + } else if (allowedAttr === "") { + return true; + } else { + const list = allowedAttr.split(","); + if (allowedType === 0 && list.includes("children")) { + return true; + } + return list.includes(MismatchTypeString[allowedType]); + } +} +var requestIdleCallback = getGlobalThis().requestIdleCallback || ((cb) => setTimeout(cb, 1)); +var cancelIdleCallback = getGlobalThis().cancelIdleCallback || ((id) => clearTimeout(id)); +var hydrateOnIdle = (timeout = 1e4) => (hydrate2) => { + const id = requestIdleCallback(hydrate2, { timeout }); + return () => cancelIdleCallback(id); +}; +function elementIsVisibleInViewport(el) { + const { top, left, bottom, right } = el.getBoundingClientRect(); + const { innerHeight, innerWidth } = window; + return (top > 0 && top < innerHeight || bottom > 0 && bottom < innerHeight) && (left > 0 && left < innerWidth || right > 0 && right < innerWidth); +} +var hydrateOnVisible = (opts) => (hydrate2, forEach) => { + const ob = new IntersectionObserver((entries) => { + for (const e of entries) { + if (!e.isIntersecting) continue; + ob.disconnect(); + hydrate2(); + break; + } + }, opts); + forEach((el) => { + if (!(el instanceof Element)) return; + if (elementIsVisibleInViewport(el)) { + hydrate2(); + ob.disconnect(); + return false; + } + ob.observe(el); + }); + return () => ob.disconnect(); +}; +var hydrateOnMediaQuery = (query) => (hydrate2) => { + if (query) { + const mql = matchMedia(query); + if (mql.matches) { + hydrate2(); + } else { + mql.addEventListener("change", hydrate2, { once: true }); + return () => mql.removeEventListener("change", hydrate2); + } + } +}; +var hydrateOnInteraction = (interactions = []) => (hydrate2, forEach) => { + if (isString(interactions)) interactions = [interactions]; + let hasHydrated = false; + const doHydrate = (e) => { + if (!hasHydrated) { + hasHydrated = true; + teardown(); + hydrate2(); + e.target.dispatchEvent(new e.constructor(e.type, e)); + } + }; + const teardown = () => { + forEach((el) => { + for (const i of interactions) { + el.removeEventListener(i, doHydrate); + } + }); + }; + forEach((el) => { + for (const i of interactions) { + el.addEventListener(i, doHydrate, { once: true }); + } + }); + return teardown; +}; +function forEachElement(node, cb) { + if (isComment(node) && node.data === "[") { + let depth = 1; + let next = node.nextSibling; + while (next) { + if (next.nodeType === 1) { + const result = cb(next); + if (result === false) { + break; + } + } else if (isComment(next)) { + if (next.data === "]") { + if (--depth === 0) break; + } else if (next.data === "[") { + depth++; + } + } + next = next.nextSibling; + } + } else { + cb(node); + } +} +var isAsyncWrapper = (i) => !!i.type.__asyncLoader; +function defineAsyncComponent(source) { + if (isFunction(source)) { + source = { loader: source }; + } + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + hydrate: hydrateStrategy, + timeout, + // undefined = never times out + suspensible = true, + onError: userOnError + } = source; + let pendingRequest = null; + let resolvedComp; + let retries = 0; + const retry = () => { + retries++; + pendingRequest = null; + return load(); + }; + const load = () => { + let thisRequest; + return pendingRequest || (thisRequest = pendingRequest = loader().catch((err) => { + err = err instanceof Error ? err : new Error(String(err)); + if (userOnError) { + return new Promise((resolve2, reject) => { + const userRetry = () => resolve2(retry()); + const userFail = () => reject(err); + userOnError(err, userRetry, userFail, retries + 1); + }); + } else { + throw err; + } + }).then((comp) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest; + } + if (!comp) { + warn$1( + `Async component loader resolved to undefined. If you are using retry(), make sure to return its return value.` + ); + } + if (comp && (comp.__esModule || comp[Symbol.toStringTag] === "Module")) { + comp = comp.default; + } + if (comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`); + } + resolvedComp = comp; + return comp; + })); + }; + return defineComponent({ + name: "AsyncComponentWrapper", + __asyncLoader: load, + __asyncHydrate(el, instance, hydrate2) { + let patched = false; + (instance.bu || (instance.bu = [])).push(() => patched = true); + const performHydrate = () => { + if (patched) { + if (true) { + warn$1( + `Skipping lazy hydration for component '${getComponentName(resolvedComp) || resolvedComp.__file}': it was updated before lazy hydration performed.` + ); + } + return; + } + hydrate2(); + }; + const doHydrate = hydrateStrategy ? () => { + const teardown = hydrateStrategy( + performHydrate, + (cb) => forEachElement(el, cb) + ); + if (teardown) { + (instance.bum || (instance.bum = [])).push(teardown); + } + } : performHydrate; + if (resolvedComp) { + doHydrate(); + } else { + load().then(() => !instance.isUnmounted && doHydrate()); + } + }, + get __asyncResolved() { + return resolvedComp; + }, + setup() { + const instance = currentInstance; + markAsyncBoundary(instance); + if (resolvedComp) { + return () => createInnerComp(resolvedComp, instance); + } + const onError = (err) => { + pendingRequest = null; + handleError( + err, + instance, + 13, + !errorComponent + ); + }; + if (suspensible && instance.suspense || isInSSRComponentSetup) { + return load().then((comp) => { + return () => createInnerComp(comp, instance); + }).catch((err) => { + onError(err); + return () => errorComponent ? createVNode(errorComponent, { + error: err + }) : null; + }); + } + const loaded = ref(false); + const error = ref(); + const delayed = ref(!!delay); + if (delay) { + setTimeout(() => { + delayed.value = false; + }, delay); + } + if (timeout != null) { + setTimeout(() => { + if (!loaded.value && !error.value) { + const err = new Error( + `Async component timed out after ${timeout}ms.` + ); + onError(err); + error.value = err; + } + }, timeout); + } + load().then(() => { + loaded.value = true; + if (instance.parent && isKeepAlive(instance.parent.vnode)) { + instance.parent.update(); + } + }).catch((err) => { + onError(err); + error.value = err; + }); + return () => { + if (loaded.value && resolvedComp) { + return createInnerComp(resolvedComp, instance); + } else if (error.value && errorComponent) { + return createVNode(errorComponent, { + error: error.value + }); + } else if (loadingComponent && !delayed.value) { + return createInnerComp( + loadingComponent, + instance + ); + } + }; + } + }); +} +function createInnerComp(comp, parent) { + const { ref: ref2, props, children, ce } = parent.vnode; + const vnode = createVNode(comp, props, children); + vnode.ref = ref2; + vnode.ce = ce; + delete parent.vnode.ce; + return vnode; +} +var isKeepAlive = (vnode) => vnode.type.__isKeepAlive; +var KeepAliveImpl = { + name: `KeepAlive`, + // Marker for special handling inside the renderer. We are not using a === + // check directly on KeepAlive in the renderer, because importing it directly + // would prevent it from being tree-shaken. + __isKeepAlive: true, + props: { + include: [String, RegExp, Array], + exclude: [String, RegExp, Array], + max: [String, Number] + }, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const sharedContext = instance.ctx; + if (!sharedContext.renderer) { + return () => { + const children = slots.default && slots.default(); + return children && children.length === 1 ? children[0] : children; + }; + } + const cache = /* @__PURE__ */ new Map(); + const keys = /* @__PURE__ */ new Set(); + let current = null; + if (true) { + instance.__v_cache = cache; + } + const parentSuspense = instance.suspense; + const { + renderer: { + p: patch, + m: move, + um: _unmount, + o: { createElement } + } + } = sharedContext; + const storageContainer = createElement("div"); + sharedContext.activate = (vnode, container, anchor, namespace, optimized) => { + const instance2 = vnode.component; + move(vnode, container, anchor, 0, parentSuspense); + patch( + instance2.vnode, + vnode, + container, + anchor, + instance2, + parentSuspense, + namespace, + vnode.slotScopeIds, + optimized + ); + queuePostRenderEffect(() => { + instance2.isDeactivated = false; + if (instance2.a) { + invokeArrayFns(instance2.a); + } + const vnodeHook = vnode.props && vnode.props.onVnodeMounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + }, parentSuspense); + if (true) { + devtoolsComponentAdded(instance2); + } + }; + sharedContext.deactivate = (vnode) => { + const instance2 = vnode.component; + invalidateMount(instance2.m); + invalidateMount(instance2.a); + move(vnode, storageContainer, null, 1, parentSuspense); + queuePostRenderEffect(() => { + if (instance2.da) { + invokeArrayFns(instance2.da); + } + const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + instance2.isDeactivated = true; + }, parentSuspense); + if (true) { + devtoolsComponentAdded(instance2); + } + if (true) { + instance2.__keepAliveStorageContainer = storageContainer; + } + }; + function unmount(vnode) { + resetShapeFlag(vnode); + _unmount(vnode, instance, parentSuspense, true); + } + function pruneCache(filter) { + cache.forEach((vnode, key) => { + const name = getComponentName( + isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : vnode.type + ); + if (name && !filter(name)) { + pruneCacheEntry(key); + } + }); + } + function pruneCacheEntry(key) { + const cached = cache.get(key); + if (cached && (!current || !isSameVNodeType(cached, current))) { + unmount(cached); + } else if (current) { + resetShapeFlag(current); + } + cache.delete(key); + keys.delete(key); + } + watch2( + () => [props.include, props.exclude], + ([include, exclude]) => { + include && pruneCache((name) => matches(include, name)); + exclude && pruneCache((name) => !matches(exclude, name)); + }, + // prune post-render after `current` has been updated + { flush: "post", deep: true } + ); + let pendingCacheKey = null; + const cacheSubtree = () => { + if (pendingCacheKey != null) { + if (isSuspense(instance.subTree.type)) { + queuePostRenderEffect(() => { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + }, instance.subTree.suspense); + } else { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + } + } + }; + onMounted(cacheSubtree); + onUpdated(cacheSubtree); + onBeforeUnmount(() => { + cache.forEach((cached) => { + const { subTree, suspense } = instance; + const vnode = getInnerChild(subTree); + if (cached.type === vnode.type && cached.key === vnode.key) { + resetShapeFlag(vnode); + const da = vnode.component.da; + da && queuePostRenderEffect(da, suspense); + return; + } + unmount(cached); + }); + }); + return () => { + pendingCacheKey = null; + if (!slots.default) { + return current = null; + } + const children = slots.default(); + const rawVNode = children[0]; + if (children.length > 1) { + if (true) { + warn$1(`KeepAlive should contain exactly one component child.`); + } + current = null; + return children; + } else if (!isVNode(rawVNode) || !(rawVNode.shapeFlag & 4) && !(rawVNode.shapeFlag & 128)) { + current = null; + return rawVNode; + } + let vnode = getInnerChild(rawVNode); + if (vnode.type === Comment) { + current = null; + return vnode; + } + const comp = vnode.type; + const name = getComponentName( + isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : comp + ); + const { include, exclude, max } = props; + if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) { + vnode.shapeFlag &= -257; + current = vnode; + return rawVNode; + } + const key = vnode.key == null ? comp : vnode.key; + const cachedVNode = cache.get(key); + if (vnode.el) { + vnode = cloneVNode(vnode); + if (rawVNode.shapeFlag & 128) { + rawVNode.ssContent = vnode; + } + } + pendingCacheKey = key; + if (cachedVNode) { + vnode.el = cachedVNode.el; + vnode.component = cachedVNode.component; + if (vnode.transition) { + setTransitionHooks(vnode, vnode.transition); + } + vnode.shapeFlag |= 512; + keys.delete(key); + keys.add(key); + } else { + keys.add(key); + if (max && keys.size > parseInt(max, 10)) { + pruneCacheEntry(keys.values().next().value); + } + } + vnode.shapeFlag |= 256; + current = vnode; + return isSuspense(rawVNode.type) ? rawVNode : vnode; + }; + } +}; +var KeepAlive = KeepAliveImpl; +function matches(pattern, name) { + if (isArray(pattern)) { + return pattern.some((p2) => matches(p2, name)); + } else if (isString(pattern)) { + return pattern.split(",").includes(name); + } else if (isRegExp(pattern)) { + pattern.lastIndex = 0; + return pattern.test(name); + } + return false; +} +function onActivated(hook, target) { + registerKeepAliveHook(hook, "a", target); +} +function onDeactivated(hook, target) { + registerKeepAliveHook(hook, "da", target); +} +function registerKeepAliveHook(hook, type, target = currentInstance) { + const wrappedHook = hook.__wdc || (hook.__wdc = () => { + let current = target; + while (current) { + if (current.isDeactivated) { + return; + } + current = current.parent; + } + return hook(); + }); + injectHook(type, wrappedHook, target); + if (target) { + let current = target.parent; + while (current && current.parent) { + if (isKeepAlive(current.parent.vnode)) { + injectToKeepAliveRoot(wrappedHook, type, target, current); + } + current = current.parent; + } + } +} +function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) { + const injected = injectHook( + type, + hook, + keepAliveRoot, + true + /* prepend */ + ); + onUnmounted(() => { + remove(keepAliveRoot[type], injected); + }, target); +} +function resetShapeFlag(vnode) { + vnode.shapeFlag &= -257; + vnode.shapeFlag &= -513; +} +function getInnerChild(vnode) { + return vnode.shapeFlag & 128 ? vnode.ssContent : vnode; +} +function injectHook(type, hook, target = currentInstance, prepend = false) { + if (target) { + const hooks = target[type] || (target[type] = []); + const wrappedHook = hook.__weh || (hook.__weh = (...args) => { + pauseTracking(); + const reset = setCurrentInstance(target); + const res = callWithAsyncErrorHandling(hook, target, type, args); + reset(); + resetTracking(); + return res; + }); + if (prepend) { + hooks.unshift(wrappedHook); + } else { + hooks.push(wrappedHook); + } + return wrappedHook; + } else if (true) { + const apiName = toHandlerKey(ErrorTypeStrings$1[type].replace(/ hook$/, "")); + warn$1( + `${apiName} is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup(). If you are using async setup(), make sure to register lifecycle hooks before the first await statement.` + ); + } +} +var createHook = (lifecycle) => (hook, target = currentInstance) => { + if (!isInSSRComponentSetup || lifecycle === "sp") { + injectHook(lifecycle, (...args) => hook(...args), target); + } +}; +var onBeforeMount = createHook("bm"); +var onMounted = createHook("m"); +var onBeforeUpdate = createHook( + "bu" +); +var onUpdated = createHook("u"); +var onBeforeUnmount = createHook( + "bum" +); +var onUnmounted = createHook("um"); +var onServerPrefetch = createHook( + "sp" +); +var onRenderTriggered = createHook("rtg"); +var onRenderTracked = createHook("rtc"); +function onErrorCaptured(hook, target = currentInstance) { + injectHook("ec", hook, target); +} +var COMPONENTS = "components"; +var DIRECTIVES = "directives"; +function resolveComponent(name, maybeSelfReference) { + return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name; +} +var NULL_DYNAMIC_COMPONENT = Symbol.for("v-ndc"); +function resolveDynamicComponent(component) { + if (isString(component)) { + return resolveAsset(COMPONENTS, component, false) || component; + } else { + return component || NULL_DYNAMIC_COMPONENT; + } +} +function resolveDirective(name) { + return resolveAsset(DIRECTIVES, name); +} +function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) { + const instance = currentRenderingInstance || currentInstance; + if (instance) { + const Component = instance.type; + if (type === COMPONENTS) { + const selfName = getComponentName( + Component, + false + ); + if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) { + return Component; + } + } + const res = ( + // local registration + // check instance[type] first which is resolved for options API + resolve(instance[type] || Component[type], name) || // global registration + resolve(instance.appContext[type], name) + ); + if (!res && maybeSelfReference) { + return Component; + } + if (warnMissing && !res) { + const extra = type === COMPONENTS ? ` +If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``; + warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`); + } + return res; + } else if (true) { + warn$1( + `resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().` + ); + } +} +function resolve(registry, name) { + return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]); +} +function renderList(source, renderItem, cache, index) { + let ret; + const cached = cache && cache[index]; + const sourceIsArray = isArray(source); + if (sourceIsArray || isString(source)) { + const sourceIsReactiveArray = sourceIsArray && isReactive(source); + let needsWrap = false; + let isReadonlySource = false; + if (sourceIsReactiveArray) { + needsWrap = !isShallow(source); + isReadonlySource = isReadonly(source); + source = shallowReadArray(source); + } + ret = new Array(source.length); + for (let i = 0, l = source.length; i < l; i++) { + ret[i] = renderItem( + needsWrap ? isReadonlySource ? toReadonly(toReactive(source[i])) : toReactive(source[i]) : source[i], + i, + void 0, + cached && cached[i] + ); + } + } else if (typeof source === "number") { + if (!Number.isInteger(source) || source < 0) { + warn$1( + `The v-for range expects a positive integer value but got ${source}.` + ); + ret = []; + } else { + ret = new Array(source); + for (let i = 0; i < source; i++) { + ret[i] = renderItem(i + 1, i, void 0, cached && cached[i]); + } + } + } else if (isObject(source)) { + if (source[Symbol.iterator]) { + ret = Array.from( + source, + (item, i) => renderItem(item, i, void 0, cached && cached[i]) + ); + } else { + const keys = Object.keys(source); + ret = new Array(keys.length); + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + ret[i] = renderItem(source[key], key, i, cached && cached[i]); + } + } + } else { + ret = []; + } + if (cache) { + cache[index] = ret; + } + return ret; +} +function createSlots(slots, dynamicSlots) { + for (let i = 0; i < dynamicSlots.length; i++) { + const slot = dynamicSlots[i]; + if (isArray(slot)) { + for (let j = 0; j < slot.length; j++) { + slots[slot[j].name] = slot[j].fn; + } + } else if (slot) { + slots[slot.name] = slot.key ? (...args) => { + const res = slot.fn(...args); + if (res) res.key = slot.key; + return res; + } : slot.fn; + } + } + return slots; +} +function renderSlot(slots, name, props = {}, fallback, noSlotted) { + if (currentRenderingInstance.ce || currentRenderingInstance.parent && isAsyncWrapper(currentRenderingInstance.parent) && currentRenderingInstance.parent.ce) { + const hasProps = Object.keys(props).length > 0; + if (name !== "default") props.name = name; + return openBlock(), createBlock( + Fragment, + null, + [createVNode("slot", props, fallback && fallback())], + hasProps ? -2 : 64 + ); + } + let slot = slots[name]; + if (slot && slot.length > 1) { + warn$1( + `SSR-optimized slot function detected in a non-SSR-optimized render function. You need to mark this component with $dynamic-slots in the parent template.` + ); + slot = () => []; + } + if (slot && slot._c) { + slot._d = false; + } + openBlock(); + const validSlotContent = slot && ensureValidVNode(slot(props)); + const slotKey = props.key || // slot content array of a dynamic conditional slot may have a branch + // key attached in the `createSlots` helper, respect that + validSlotContent && validSlotContent.key; + const rendered = createBlock( + Fragment, + { + key: (slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) + // #7256 force differentiate fallback content from actual content + (!validSlotContent && fallback ? "_fb" : "") + }, + validSlotContent || (fallback ? fallback() : []), + validSlotContent && slots._ === 1 ? 64 : -2 + ); + if (!noSlotted && rendered.scopeId) { + rendered.slotScopeIds = [rendered.scopeId + "-s"]; + } + if (slot && slot._c) { + slot._d = true; + } + return rendered; +} +function ensureValidVNode(vnodes) { + return vnodes.some((child) => { + if (!isVNode(child)) return true; + if (child.type === Comment) return false; + if (child.type === Fragment && !ensureValidVNode(child.children)) + return false; + return true; + }) ? vnodes : null; +} +function toHandlers(obj, preserveCaseIfNecessary) { + const ret = {}; + if (!isObject(obj)) { + warn$1(`v-on with no argument expects an object value.`); + return ret; + } + for (const key in obj) { + ret[preserveCaseIfNecessary && /[A-Z]/.test(key) ? `on:${key}` : toHandlerKey(key)] = obj[key]; + } + return ret; +} +var getPublicInstance = (i) => { + if (!i) return null; + if (isStatefulComponent(i)) return getComponentPublicInstance(i); + return getPublicInstance(i.parent); +}; +var publicPropertiesMap = ( + // Move PURE marker to new line to workaround compiler discarding it + // due to type annotation + extend(/* @__PURE__ */ Object.create(null), { + $: (i) => i, + $el: (i) => i.vnode.el, + $data: (i) => i.data, + $props: (i) => true ? shallowReadonly(i.props) : i.props, + $attrs: (i) => true ? shallowReadonly(i.attrs) : i.attrs, + $slots: (i) => true ? shallowReadonly(i.slots) : i.slots, + $refs: (i) => true ? shallowReadonly(i.refs) : i.refs, + $parent: (i) => getPublicInstance(i.parent), + $root: (i) => getPublicInstance(i.root), + $host: (i) => i.ce, + $emit: (i) => i.emit, + $options: (i) => __VUE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type, + $forceUpdate: (i) => i.f || (i.f = () => { + queueJob(i.update); + }), + $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)), + $watch: (i) => __VUE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP + }) +); +var isReservedPrefix = (key) => key === "_" || key === "$"; +var hasSetupBinding = (state, key) => state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key); +var PublicInstanceProxyHandlers = { + get({ _: instance }, key) { + if (key === "__v_skip") { + return true; + } + const { ctx, setupState, data, props, accessCache, type, appContext } = instance; + if (key === "__isVue") { + return true; + } + if (key[0] !== "$") { + const n = accessCache[key]; + if (n !== void 0) { + switch (n) { + case 1: + return setupState[key]; + case 2: + return data[key]; + case 4: + return ctx[key]; + case 3: + return props[key]; + } + } else if (hasSetupBinding(setupState, key)) { + accessCache[key] = 1; + return setupState[key]; + } else if (__VUE_OPTIONS_API__ && data !== EMPTY_OBJ && hasOwn(data, key)) { + accessCache[key] = 2; + return data[key]; + } else if (hasOwn(props, key)) { + accessCache[key] = 3; + return props[key]; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if (!__VUE_OPTIONS_API__ || shouldCacheAccess) { + accessCache[key] = 0; + } + } + const publicGetter = publicPropertiesMap[key]; + let cssModule, globalProperties; + if (publicGetter) { + if (key === "$attrs") { + track(instance.attrs, "get", ""); + markAttrsAccessed(); + } else if (key === "$slots") { + track(instance, "get", key); + } + return publicGetter(instance); + } else if ( + // css module (injected by vue-loader) + (cssModule = type.__cssModules) && (cssModule = cssModule[key]) + ) { + return cssModule; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if ( + // global properties + globalProperties = appContext.config.globalProperties, hasOwn(globalProperties, key) + ) { + { + return globalProperties[key]; + } + } else if (currentRenderingInstance && (!isString(key) || // #1091 avoid internal isRef/isVNode checks on component instance leading + // to infinite warning loop + key.indexOf("__v") !== 0)) { + if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) { + warn$1( + `Property ${JSON.stringify( + key + )} must be accessed via $data because it starts with a reserved character ("$" or "_") and is not proxied on the render context.` + ); + } else if (instance === currentRenderingInstance) { + warn$1( + `Property ${JSON.stringify(key)} was accessed during render but is not defined on instance.` + ); + } + } + }, + set({ _: instance }, key, value) { + const { data, setupState, ctx } = instance; + if (hasSetupBinding(setupState, key)) { + setupState[key] = value; + return true; + } else if (setupState.__isScriptSetup && hasOwn(setupState, key)) { + warn$1(`Cannot mutate + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..242c115 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6230 @@ +{ + "name": "all-docs", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "all-docs", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "tsx": "^4.21.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.59.2", + "vite": "^5.4.21", + "vitepress": "^1.6.3", + "vitepress-plugin-llms": "^1.12.2" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.1.tgz", + "integrity": "sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.1.tgz", + "integrity": "sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.1.tgz", + "integrity": "sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.1.tgz", + "integrity": "sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.1.tgz", + "integrity": "sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.1.tgz", + "integrity": "sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.1.tgz", + "integrity": "sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.1.tgz", + "integrity": "sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.1.tgz", + "integrity": "sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.1.tgz", + "integrity": "sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.1.tgz", + "integrity": "sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.1.tgz", + "integrity": "sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.1.tgz", + "integrity": "sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.1.tgz", + "integrity": "sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "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.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/js/node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@docsearch/js/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "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", + "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", + "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-array/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/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "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/@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.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "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.1.1", + "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/eslintrc/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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "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/@humanfs/core": { + "version": "0.19.2", + "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", + "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", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "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", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.82", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.82.tgz", + "integrity": "sha512-4p978qHx8eD/QBOhgBzp/p7uS3OO2KCnVpFPJTUvuhuDXv1Hr4RcxcZ5MWc6ptkf/3Dlb1xb23068OtPyx10mA==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "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", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "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/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.4", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/react": { + "version": "19.2.14", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "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.59.3", + "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.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "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.59.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "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.59.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "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.59.3", + "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.59.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "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.59.3", + "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.59.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "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/semver": { + "version": "7.8.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.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/visitor-keys": { + "version": "8.59.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "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/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "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/algoliasearch": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.1.tgz", + "integrity": "sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/abtesting": "1.18.1", + "@algolia/client-abtesting": "5.52.1", + "@algolia/client-analytics": "5.52.1", + "@algolia/client-common": "5.52.1", + "@algolia/client-insights": "5.52.1", + "@algolia/client-personalization": "5.52.1", + "@algolia/client-query-suggestions": "5.52.1", + "@algolia/client-search": "5.52.1", + "@algolia/ingestion": "1.52.1", + "@algolia/monitoring": "1.52.1", + "@algolia/recommend": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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.30001792", + "dev": true, + "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/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "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.5", + "@eslint/js": "9.39.4", + "@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-plugin-react-hooks": { + "version": "7.1.1", + "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-plugin-react-refresh": { + "version": "0.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "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": "5.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/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/eslint/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/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/espree/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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "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", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "dev": true, + "license": "ISC" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "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/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "optional": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdown-title": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/markdown-title/-/markdown-title-1.0.2.tgz", + "integrity": "sha512-MqIQVVkz+uGEHi3TsHx/czcxxCbRIL7sv5K5DnYw/tI+apY54IbPefV/cmgxp6LoJSEx/TqcHdLs/298afG5QQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/millify": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/millify/-/millify-6.1.0.tgz", + "integrity": "sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "yargs": "^17.0.1" + }, + "bin": { + "millify": "bin/millify" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "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/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.44", + "dev": true, + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "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/p-limit": { + "version": "3.1.0", + "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", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz", + "integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.6", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "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/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "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/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "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/tokenx": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tokenx/-/tokenx-1.3.0.tgz", + "integrity": "sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.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/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", + "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "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", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vitepress-plugin-llms": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/vitepress-plugin-llms/-/vitepress-plugin-llms-1.12.2.tgz", + "integrity": "sha512-hdklo7di6E2/MwlYstH7R5QgdPHX+f5G/fp8QBq6MCiw4DWi3IOKMaous0S839FIGsPjK3QHwK6KcFwCf/XzjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "gray-matter": "^4.0.3", + "markdown-it": "^14.1.0", + "markdown-title": "^1.0.2", + "mdast-util-from-markdown": "^2.0.3", + "millify": "^6.1.0", + "minimatch": "^10.2.5", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "pretty-bytes": "^7.1.0", + "remark": "^15.0.1", + "remark-frontmatter": "^5.0.0", + "tokenx": "^1.3.0", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/okineadev" + } + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..995c42c --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "all-docs", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "npm run site:generate && tsc -b && vite build", + "docs:prepare:slm-design": "tsx scripts/docs/prepare.ts slm-design", + "docs:build:slm-design": "npm run docs:prepare:slm-design && vitepress build docs/slm-design", + "site:generate": "tsx scripts/site/generate-artifacts.ts", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "tsx": "^4.21.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.59.2", + "vite": "^5.4.21", + "vitepress": "^1.6.3", + "vitepress-plugin-llms": "^1.12.2" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/docs/prepare.ts b/scripts/docs/prepare.ts new file mode 100644 index 0000000..e0bb10b --- /dev/null +++ b/scripts/docs/prepare.ts @@ -0,0 +1,85 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +type Page = { + source: string; + target: string; +}; + +type DocsConfig = { + mounts: Page[]; +}; + +const MD_LINK_RE = /\]\((?!#|[a-z][a-z0-9+.-]*:)([^)\s]+\.md)(#[^)]*)?\)/gi; + +const siteName = process.argv[2]; + +if (!siteName) { + throw new Error('Укажите имя сайта: tsx scripts/docs/prepare.ts slm-design'); +} + +const rootDir = process.cwd(); +const canonsDir = path.join(rootDir, 'canons'); +const siteDir = path.join(rootDir, 'docs', siteName); +const contentDir = path.join(siteDir, 'content'); +const configPath = path.join(siteDir, 'docs.config.ts'); + +if (!fs.existsSync(configPath)) { + throw new Error(`Не найден конфиг сайта: ${configPath}`); +} + +const config = (await import(pathToFileURL(configPath).href)) as DocsConfig; +const targetBySource = new Map( + config.mounts.map((page) => [normalizePath(page.source), normalizePath(page.target)]), +); + +function normalizePath(value: string) { + return value.split(path.sep).join('/').replace(/^\.\//, ''); +} + +function formatRelativeMarkdownPath(fromTarget: string, toTarget: string) { + const relative = path + .relative(path.dirname(fromTarget), toTarget) + .split(path.sep) + .join('/'); + + return relative.startsWith('.') ? relative : `./${relative}`; +} + +function transformMarkdownLinks(content: string, page: Page) { + const sourceDir = path.posix.dirname(normalizePath(page.source)); + + return content.replace(MD_LINK_RE, (match, href: string, hash = '') => { + const [hrefPath, query = ''] = href.split('?'); + const sourcePath = normalizePath(path.posix.normalize(path.posix.join(sourceDir, hrefPath))); + const target = targetBySource.get(sourcePath); + + if (!target) return match; + + const nextHref = formatRelativeMarkdownPath(page.target, `${target}${query ? `?${query}` : ''}`); + + return `](${nextHref}${hash})`; + }); +} + +fs.rmSync(contentDir, { recursive: true, force: true }); +fs.mkdirSync(contentDir, { recursive: true }); + +for (const page of config.mounts) { + const sourcePath = path.join(canonsDir, page.source); + const targetPath = path.join(contentDir, page.target); + + if (!fs.existsSync(sourcePath)) { + throw new Error(`Не найден канон: ${sourcePath}`); + } + + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + + const content = transformMarkdownLinks(fs.readFileSync(sourcePath, 'utf8'), page); + fs.writeFileSync(targetPath, content, 'utf8'); + + console.log(`${page.target} -> canons/${page.source}`); +} + +console.log(`Подготовлен VitePress content для ${siteName}: ${config.mounts.length} страниц`); diff --git a/scripts/site/generate-artifacts.ts b/scripts/site/generate-artifacts.ts new file mode 100644 index 0000000..1fefdee --- /dev/null +++ b/scripts/site/generate-artifacts.ts @@ -0,0 +1,65 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { docs } from '../../src/config/docs.config'; + +const siteTitle = 'Документация'; +const siteDescription = 'Единое пространство для идей, черновиков и первых версий документаций, которые ещё формируются и постепенно становятся самостоятельными материалами.'; + +const rootDir = process.cwd(); +const publicDir = path.join(rootDir, 'public'); +const llmsPath = path.join(publicDir, 'llms.txt'); + +function formatMarkdownLink(label: string, href: string, description: string) { + return `- [${label}](${href}): ${description}`; +} + +function findDocLink(doc: (typeof docs)[number], label: string) { + return doc.links.find((link) => link.label === label); +} + +function formatLlmsLinks() { + return docs + .map((doc) => { + const link = findDocLink(doc, 'llms.txt'); + + if (!link) return undefined; + + return formatMarkdownLink(doc.title, link.href, doc.description); + }) + .filter(Boolean); +} + +function formatFullLinks() { + return docs + .map((doc) => { + const link = findDocLink(doc, 'llms-full.txt'); + + if (!link) return undefined; + + return formatMarkdownLink(`${doc.title} full`, link.href, `Полный bundle документации: ${doc.label.toLowerCase()}.`); + }) + .filter(Boolean); +} + +const content = [ + `# ${siteTitle}`, + '', + `> ${siteDescription}`, + '', + 'Этот файл является корневой картой документаций. Для работы с конкретным направлением используйте его собственный `llms.txt`.', + '', + '## Documentation', + '', + ...formatLlmsLinks(), + '', + '## Optional', + '', + ...formatFullLinks(), + '', +].join('\n'); + +fs.mkdirSync(publicDir, { recursive: true }); +fs.writeFileSync(llmsPath, content, 'utf8'); + +console.log(`Сгенерирован ${path.relative(rootDir, llmsPath)}`); diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..5fc404c --- /dev/null +++ b/src/App.css @@ -0,0 +1,465 @@ +.page { + display: flex; + flex-direction: column; + justify-content: center; + gap: 64px; + min-height: 100vh; + padding: 48px 32px; + background: + radial-gradient(circle at 50% 18%, var(--hero-glow), transparent 34%), + linear-gradient(var(--grid-line) 1px, transparent 1px), + linear-gradient(90deg, var(--grid-line) 1px, transparent 1px), + var(--page-bg); + background-size: auto, 32px 32px, 32px 32px, auto; + background-position: center, center, center, center; + color: var(--text-primary); +} + +.hero { + text-align: center; +} + +.title { + margin: 0 0 16px; + color: var(--accent-cool); + font-size: 56px; + font-weight: 750; + letter-spacing: -0.035em; + line-height: 1; +} + +.lead { + max-width: 760px; + margin: 0 auto; + color: var(--text-secondary); + font-size: 18px; + line-height: 1.55; +} + +.controls { + display: inline-flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 10px; + margin-top: 28px; +} + +.repoLink { + display: inline-flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 14px; + border: 1px solid var(--border-soft); + border-radius: 999px; + color: var(--text-secondary); + background: var(--surface-soft); + box-shadow: 0 1px 2px var(--shadow-subtle); + font-size: 13px; + font-weight: 700; + text-decoration: none; + transition: border-color 150ms ease, color 150ms ease, background 150ms ease; +} + +.repoLink:hover { + border-color: color-mix(in srgb, var(--accent-cool) 46%, var(--border-soft)); + color: var(--text-primary); + background: color-mix(in srgb, var(--surface-soft) 92%, var(--accent-cool)); +} + +.repoLink:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 3px; +} + +.themeToggle { + display: inline-flex; + align-items: stretch; + gap: 2px; + height: 36px; + padding: 4px; + border: 1px solid var(--border-soft); + border-radius: 999px; + background: var(--surface-soft); + box-shadow: 0 1px 2px var(--shadow-subtle); +} + +.themeToggleOption { + appearance: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + padding: 0; + border: 0; + border-radius: 999px; + color: var(--text-muted); + background: transparent; + font: inherit; + cursor: pointer; + transition: background-color 150ms ease, color 150ms ease, box-shadow 150ms ease; +} + +.themeToggleOption:hover { + color: var(--text-primary); +} + +.themeToggleOption:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 3px; +} + +.themeToggleOption[data-active='true'] { + color: var(--text-primary); + background: var(--page-bg); + box-shadow: 0 1px 2px var(--shadow-subtle); +} + +.themeToggleIcon { + display: inline-flex; + width: 16px; + height: 16px; +} + +.themeToggleIcon svg { + width: 100%; + height: 100%; +} + +.docsPanel { + display: grid; + gap: 14px; + width: 100%; + max-width: 920px; + margin: 0 auto; +} + +.docsPanelHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 0 4px 2px; + color: var(--text-muted); + font-size: 12px; + font-weight: 650; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.docItem { + --doc-accent: var(--accent-cool); + + position: relative; + display: grid; + gap: 18px; + padding: 24px; + overflow: hidden; + border: 1px solid var(--border-soft); + border-radius: 20px; + color: inherit; + background: color-mix(in srgb, var(--surface-soft) 98%, var(--doc-accent)); + box-shadow: 0 18px 54px rgb(0 0 0 / 9%); + transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease; +} + +.docItem[data-accent='blue'] { + --doc-accent: #62a5ff; +} + +.docItem[data-accent='cyan'] { + --doc-accent: #57d6ff; +} + +.docItem[data-accent='pink'] { + --doc-accent: #ff72c6; +} + +.docItem:hover { + border-color: color-mix(in srgb, var(--doc-accent) 58%, var(--border-soft)); + background: color-mix(in srgb, var(--surface-soft) 94%, var(--doc-accent)); + box-shadow: + 0 18px 54px rgb(0 0 0 / 10%), + 0 0 0 1px color-mix(in srgb, var(--doc-accent) 18%, transparent), + 0 0 36px color-mix(in srgb, var(--doc-accent) 14%, transparent); +} + +.docCardLink { + position: absolute; + inset: 0; + z-index: 1; + border-radius: inherit; +} + +.docCardLink:focus-visible { + outline: 2px solid var(--doc-accent); + outline-offset: -3px; +} + +.docMain { + position: relative; + z-index: 0; + display: grid; + grid-template-columns: 64px minmax(0, 1fr); + gap: 18px; + align-items: center; +} + +.docMark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + color: var(--doc-accent); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 13px; + font-weight: 760; + letter-spacing: -0.03em; +} + +.docIcon { + width: 64px; + height: 64px; +} + +.docIconSlm { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.35; +} + +.docIconSlm circle { + fill: currentColor; + stroke: none; +} + +.docIconNext path:first-child { + fill: currentColor; +} + +.docIconNext path:last-child { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-width: 1.8; +} + +.docIconReact { + fill: none; + stroke: currentColor; + stroke-width: 1.25; +} + +.docIconReact circle { + fill: currentColor; + stroke: none; +} + +.docIconFigma { + fill: none; + stroke: currentColor; + stroke-width: 1.3; +} + +.docIconFigma circle { + fill: color-mix(in srgb, currentColor 18%, transparent); +} + +.docMeta { + margin-bottom: 5px; + color: var(--doc-accent); + font-size: 11px; + font-weight: 760; + letter-spacing: 0.09em; + line-height: 1; + text-transform: uppercase; +} + +.docItem h2 { + margin: 0; + color: var(--text-primary); + font-size: 21px; + font-weight: 760; + letter-spacing: -0.03em; + line-height: 1.15; +} + +.docItem p { + max-width: 560px; + margin: 8px 0 0; + color: var(--text-secondary); + font-size: 14px; + line-height: 1.55; +} + +.docActions { + position: relative; + z-index: 2; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + padding-top: 16px; + border-top: 1px solid var(--border-soft); + pointer-events: none; +} + +.docStatus, +.docLink { + display: inline-flex; + align-items: center; + text-decoration: none; + pointer-events: auto; +} + +.docStatus { + gap: 8px; + padding: 6px 10px; + border: 1px solid color-mix(in srgb, var(--doc-accent) 34%, var(--border-soft)); + border-radius: 10px; + color: var(--doc-accent); + background: color-mix(in srgb, var(--surface-soft) 90%, var(--doc-accent)); + font-size: 12px; + font-weight: 700; + transition: border-color 150ms ease, background 150ms ease; +} + +.docStatusLink { + padding: 7px 12px; + border-color: color-mix(in srgb, var(--accent-cool) 58%, var(--border-soft)); + border-radius: 10px; + color: var(--accent-cool); + background: color-mix(in srgb, var(--surface-soft) 88%, var(--accent-cool)); + font-size: 13px; +} + +.docStatusLink:hover { + border-color: var(--accent-cool); + background: color-mix(in srgb, var(--surface-soft) 80%, var(--accent-cool)); +} + +.docItem[data-state='planned'] .docStatus { + color: var(--text-muted); +} + +.docLinks { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.docLink { + padding: 6px 10px; + border: 1px solid var(--border-soft); + border-radius: 8px; + color: var(--text-primary); + background: var(--page-bg); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; + font-weight: 500; + transition: border-color 150ms ease, color 150ms ease; +} + +.docLink:hover { + border-color: var(--doc-accent); + color: var(--doc-accent); +} + +.footer { + margin: 0; + color: var(--text-muted); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; + text-align: center; +} + +@media (max-width: 768px) { + .page { + justify-content: flex-start; + gap: 40px; + padding: 48px 20px 56px; + } + + .title { + font-size: 38px; + } + + .lead { + font-size: 16px; + } + + .docItem { + gap: 16px; + } + + .docActions { + align-items: flex-start; + flex-direction: column; + } +} + +@media (max-width: 480px) { + .page { + gap: 36px; + padding: 44px 16px 48px; + } + + .title { + font-size: 32px; + } + + .lead { + font-size: 15px; + line-height: 1.5; + } + + .docItem { + padding-right: 16px; + padding-left: 16px; + } + + .docsPanelHeader { + padding-right: 4px; + padding-left: 4px; + } + + .docMain { + grid-template-columns: 42px minmax(0, 1fr); + gap: 12px; + } + + .docMark { + width: 42px; + height: 42px; + font-size: 12px; + } + + .docIcon { + width: 30px; + height: 30px; + } + + .docItem h2 { + font-size: 19px; + } + + .docStatus, + .docLinks, + .docLink { + width: 100%; + } + + .docStatus, + .docLink { + justify-content: center; + } + + .docLink { + flex: 1; + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..51d8632 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,290 @@ +import { useEffect, useLayoutEffect, useState } from 'react' + +import { docs } from './config/docs.config' +import './App.css' + +type ThemeMode = 'system' | 'dark' | 'light' +type ResolvedTheme = Exclude + +const DARK_THEME_QUERY = '(prefers-color-scheme: dark)' +const THEME_STORAGE_KEY = 'all-docs-theme' +const repositoryUrl = 'https://gromlab.ru/gromov/docs' + +const themeOptions: ReadonlyArray<{ + value: Exclude + ariaLabel: string + title: string +}> = [ + { + value: 'light', + ariaLabel: 'Включить светлую тему', + title: 'Светлая', + }, + { + value: 'dark', + ariaLabel: 'Включить тёмную тему', + title: 'Тёмная', + }, +] + +const themeIconByMode = { + light: ( + + ), + dark: ( + + ), +} + +const canUseDom = () => typeof window !== 'undefined' && typeof document !== 'undefined' + +const isThemeMode = (value: string | null): value is ThemeMode => { + return value === 'system' || value === 'dark' || value === 'light' +} + +const getSystemTheme = (): ResolvedTheme => { + if (!canUseDom()) return 'light' + + return window.matchMedia(DARK_THEME_QUERY).matches ? 'dark' : 'light' +} + +const resolveTheme = (theme: ThemeMode): ResolvedTheme => { + return theme === 'system' ? getSystemTheme() : theme +} + +const getStoredTheme = (): ThemeMode => { + if (!canUseDom()) return 'dark' + + const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY) + + return isThemeMode(storedTheme) ? storedTheme : 'dark' +} + +const applyTheme = (theme: ThemeMode) => { + if (!canUseDom()) return + + const resolvedTheme = resolveTheme(theme) + + document.documentElement.dataset.theme = theme + document.documentElement.style.colorScheme = resolvedTheme + + if (theme === 'system') { + window.localStorage.removeItem(THEME_STORAGE_KEY) + return + } + + window.localStorage.setItem(THEME_STORAGE_KEY, theme) +} + +function useTheme() { + const [theme, setTheme] = useState(getStoredTheme) + const [resolvedTheme, setResolvedTheme] = useState(() => resolveTheme(getStoredTheme())) + + useLayoutEffect(() => { + applyTheme(theme) + setResolvedTheme(resolveTheme(theme)) + }, [theme]) + + useEffect(() => { + if (!canUseDom()) return undefined + + const mediaQuery = window.matchMedia(DARK_THEME_QUERY) + const handleSystemThemeChange = () => { + if (theme !== 'system') return + + const nextResolvedTheme = resolveTheme(theme) + + document.documentElement.style.colorScheme = nextResolvedTheme + setResolvedTheme(nextResolvedTheme) + } + + mediaQuery.addEventListener('change', handleSystemThemeChange) + + return () => mediaQuery.removeEventListener('change', handleSystemThemeChange) + }, [theme]) + + return { theme, resolvedTheme, setTheme } +} + +function ThemeToggle() { + const { theme, resolvedTheme, setTheme } = useTheme() + const toggleTheme = (value: Exclude) => { + setTheme(theme === value ? 'system' : value) + } + + return ( +
+ {themeOptions.map((option) => ( + + ))} +
+ ) +} + +function GithubIcon() { + return ( + + ) +} + +function DocIcon({ mark }: { mark: string }) { + if (mark === 'SLM') { + return ( + + ) + } + + if (mark === 'NX') { + return ( + + ) + } + + if (mark === 'RE') { + return ( + + ) + } + + if (mark === 'FG') { + return ( + + ) + } + + return {mark} +} + +function App() { + return ( +
+
+

Документация

+

+ Единое пространство для идей, черновиков и первых версий документаций, + которые ещё формируются и постепенно становятся самостоятельными материалами. +

+ +
+ +
+
+ Документация + {docs.length} направления +
+ + {docs.map((doc) => { + const isAvailable = Boolean(doc.href) + + return ( + + ) + })} +
+ +
Рабочее пространство документаций
+
+ ) +} + +export default App diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 zcmV+cGycqpP)V|)f$;Qooc7=_G zlYe)HToTQIc!$)^+J1M1y0*T%w!p~7%ux`!eRhO?c80XDxKQ*R^lUUMnA>6NT^?feoZ8xxvP32D&s-9ow zqjcM}eesrC)NeDmsf)*P7wJ|K!&xP%Zy4iI8lF)Tv2!reW)tCzg_1=PmOwd1SQfxa z8;58t!=z~Ba7CYlNWVG>he8aRPY|+-JmozNhn!#9i#77Aa_Edt$ijyCWL#=~I>~2X zZNrQ8I0=D+NWD4pq=7~(i zhfThMNw|G>g^y9pGzxX7ZSApl@tIxFcs{p#MX{Ax&XZT+cR#U+OWc@S)pkIuI}dzu zH?^Q=<(y&Vq-oxSLfc0Zmq81bjZWf}RnssBaD6}2g-XJHLcN_|*IOu>m|x$nbm(?E zyNy!Zp=RroS;?Vg*kmoJYBi!n5{_^@rA!)=t#a^;N$8GL!*DsQb}`yvEuX!G@||An znOfUZAevPrkV_qjl|<~3QRZzG&h@C9Y5z zqpNH4xqbF_InIPh)kX}Vn^5kyed|mOuq+2>M;v~KO37a#yrEn3XDqtOl=rc6_KZ!; zreo)DFVB4|>1Zd(bvMI%8uM;3!)YMYu&cG?(PE!B~y@3yKBMt|R zAf=I16tFwPsl)!jDqvYkLHaAQ+f@W1m6F5aZvwhm4JL z{_l)@b;)mDSzle2gyFP5-r1x-5X{G}ot%VyWP@vEW80!Q=f%RTfpg>B*TA^pyWYUQ z<=xPtz}WcZ!;rFl4m1D&FFHv?K~#9!?A%+fn=lXt;9!Fc#kQ;zk~gZFsH z8e5iu@c_pzX&qb8&Dum*oXwB+fm6l6gFfC|o*wgEiy6tw~&co z9Vd_4)P%wP-KwQW7|lN-znGK#?N+j24U=$982myIBM+vsiKsc*@4-rwJxuAaHKna6 zT3wi!C~a4ZKH03qU}_1bKyx0&$CaK7_%Z+Kl$)fF5^op zZApQF2TvDav!s|krTjw-8US6ep z%!VmX4luub+fseQz_D9ATJQ?iQQwD}TZz{-yo#l12a%+7bT@E(X-hyaVS-5vuXc#^ zx^w;L21;NphGVoj*{s3f4dme0y2LC=G1-7THd`#z?;tuC{^9k(dM{Rf2GOxg7Jzho z7nSZHl7?M9kdalX`)YgoKEfiae5+;$(OGeN1eqxrv!ZCVKyH>xiyNqfe8xzY8*7)H zQls8KMp)F4D>ED;idMOU^^WhVF@q>ZSmeB0y~qC~|DB648hr%Sh|*T(4q|w2l?m2+ zvBVw3@7+Mz?^Yc#+se6KM;a<=(W-I>k)$-qL2V*t}VaW`;?P4)WqI%maIDq8!oUcSYAD`}wWjkSyAVsnF65#2zQ zZ>(K*TlS(E#4y$4Zq+e^_&}d)q20hCe3!LfLYP%nQpLJ~gM6a1hJlz3)aS<9C9me| zAcmJ#>tOwBy{HoP0Sm1&_(E+S@6 zgBIFUoei8zJmdpiq8q5=OY7t@`)JWxn_&GvKVr=Zdb_pEL_j|=?f;WK^U9Q0efd#K z9q7SfJTl4pmA$jsZ5oK8@O9#!I3Cv-kL)<8SalSsp#dcpvJ}Nz#G6FC0%9|7Fi#8; zGDJXtj!&GljT3*HE@0EE>G8Se&d)*nkqe}-?`3vPl&UqK?xG z!3XJ4M-x`EuQjhBbu?ik-)rmIt=DF_N?TVMP)8Gjn)TZ2V%H|zENbeix}kOxd@0}Q z>)HuH6Ean!uS#~4g2Ne2WsMGel|h%j9*W_quQheG^JqmKhc*RYzp0wKlGjBq2VzY_ zgOv8WC1+%W=W)k)Yp_`8kfE=uiiwOZTXi8Uj9YGr$f@yJcJ;#&-Nq~sJ7anE(@;QN z=~br%7%7`isKStX|7!1?L(apl^QvPKlrHV4S+6tNVQ*R1iGdC~WMNE1$a+=rpQmcB z>wxiLIBvOnm;u*;9Y!kJdy(T4lk|8>JAm(&wEsFIF1$_*{>2ZNd$V6DS=SfrGxAv0 zzKe377JI`&o9Ljr+VnS*EwehA{f&{cKZF(6*MG5!p5MvrFA3ll{fmRG*L@6^cb;o^ z3Wm8c?Sc6$`>~VEWw(c$Y?nRO;2Q$=ulpqPtM^=1IZx;@xK0PgO7rKQ^WHVLwtgUT z%|JF{^f(VH)wLKQ%dYiu2RmchBdxL0-M?wxxul_z*{h6ZZ`>-k(vizs((vW8Lt6Z6 zY;Dt?@JWyN`O`f;&d1Mb?e%9oyRK1ql?EE5XB2(W)|D1~Rx35$H6@6)$F?)7V|zEO zI}fu0-0}8W5=6sg$fPnZ~7=tTudl?Ecb@pxbo)vni%gP-?hL|%*?62C;x6?@E`VRnJv z?fTb;k4x;TS7Cu-z%J}uy}e-pwpLQ17Q@4DC+FCdAmNKklG$`I_pyw7E{fYmw~{Fj zi?6KcVy=Wrel)EB_DWO|0CKmI|13!gBV?X`Ozp7x>?6jr`>Qz=^4ea35!$*f}) zS$i+x_k+@P2q1RFUH^ZTTk7=n?cjfR>hTq3l3SY~#w+I8SSutXGyhw;Ws~=zMQ%Vc z>$On~47Ut?P*_!TOQ&PFmLAyJieB2X4_Fd_!WxI-AY`q1Lc-oK?+qcOTzlQ?@~x@OT}*9jTVNfl@3rGvZpWI=eKg>T zZb@6YWz)J=IhP7CF|c?G62vMEG%#U}?#86$0jR4sG~i(jRd#jmn`7b(O#?N;3a;1t zhXLssmUwGhp79luw#(*V8WL0|8+E z6=YZ_O@er~$LrD_PYGc(kJgB=;yw#+Z3X6LDUZ(NcwN=B-hjdiHm!JFar%m{(5bEW z@@_VEtG$5;`EJZ|OkJ@l&G9n((w@uNFwmU%bG|s#TbcJJos!{e+bjCjrCq_}LcN!UFgKtgg7siV*7# z!}1whTRRi*-avJPu->C}Z8EiuK$#886+H_#_!btv+rsiBbv2jAJvJ+O0{#}y(%L3H zfjU-kq_-L@2XrL*ae{{qYJkD{@dw%*bkh2P&YS-0!Xt!PRz7KHV0+~j(t9W8lAVWR zt@B*DgURgEz4>WuN>o?_iKcw$?k{||Pg7{Q2o4|VmJ)mg?{VQJA<}zEr^YAAS zgGm5RT4T3p)U;yz-tfBO^kw8?IoG!IVmc+Z3m#}AOQ?5MRa>)OcU!$N^_+yK6ayn? zK>~WK0!#ysuj^oNLakm)Zvu+J)OSubX^kv!c*xgdIvs;kln!rgG4*uZ;w0mQQO4XD zO9P{GNdv!=cQ(CAL{S(%KtuV^zC&Q{%g)PoXnp^gn^>c*`E>$hLYg2HjnbVGtWLa{7zHdG1jT@B{|Dm16 z7K2(jsfG+m*Zxof)iXxu+!H5Mo-0$pkyV3VV4B@Qms46M zuBxGRV@HxU7Wwx-6CB zaU*HO<_qn$5GH>&@?nRy1{z zkik!sLfWQ)r#75)vVwCBU*r_)Q6mp?!j85{#Xqse)ApRdE$V0%I0*~e(_{)5H)`Mk z#rExC>yjhZxuL@|+#v4#<Axw$+VpV zuT;!2Vww$je$DpAW`$FX_Ab|Ip%$;&T$-lW8jS~B$>G}rd>eQG+$h9lQx4Mx0w={m zx9?T6VU`>sR}XClkAhHEShOUe8awiq zmizhL+}5UKs3}6~It7vBTig9dfQ2Q8coo+Miiaw7n~>4ybv2Ptt0^^=VqX(t*Yya9 zr`FxxFX8(v*H=+uJ#JJWIB2A(==HDYx~^zZ2nu?2`}|Wsa*f3h3ixc+U|FDtAG$Y! z*lc_7se5Oso-Cgqe0){{!8H4g$3<8!R<6JOurD;((({c$1(pwb>(#TT!sge@4>r2@ zVL7>U`0`nsWAYErezk4(Z!gMI2?UTo{J3Ajo(u4)KYIRd>BRcG4BoS3G0EXyEp@tw z%P7__?A^a>Q&AKL@ayDO9D*Qkc!NHnO9l}kpp_6hXbMppYL(X1L?njdFT|-h2<_$; zAtDZ!1Rf%|yb!qbWKd}%0b`LzBeyNy43|QO(&h2mxQLUL)|0%agVOW)6TV!&Ip^Ls z`PG2cygM8)IecQx=Fc+nqYRo4hS^^-nM_&-y8?EJXUczP=DIw(GkTJdpEdh<_STs{ z|A)4n1GKdE=Wu!!nYoZHcUQ4S&R;oDOKX2lrkdF(mK>hz<$Pp>igjOcvoRIjlN=W8 zu8Gx5(roqn8$>gEE5vy{GiGeW8Tq{vnf3hS-V=$tZkQuftUVuU8o6k&dn=Yg3)6MOIH>nlK^-2+C6BZITr~1@So?NvG#TwL)|~=1YXGMTLpS<)ziK_CSOabe z=cB#5)yz|@0i9dSo?*CX)}UP=s6)B+F@~Em(u@Q(I9J9i_V{LmMu8BfXYMh~*oPP+ z!3~xTv|(>|=n6ZOtT~C@V!z!w%18*8T2t6}U2S##rC)mekBql&VsBX;$~ByGE$oA9 z`0Wzq8p?R{4)$l*on;!cLa}Dh^Xe?owiQZt9nH1fxxh$pN9K%CtOw?u3>85L7rr!d zXs)l{TZ{xXP&U8exz?9cv~dNNibOmt*K4I$?RxqIBZ0(?Mg-9FS{*9Bc49Qc1`=sIF-rye`aNT1G@4NwXcnyc@+bw_mTsR>5< zF<2;X0QesG_pw|TonqVBhRtfqI>ty(SIu&VOXd0CrLlfp+;WH7HYjhqnu^oAY!9cB z=B6#R?Rfz9BP`dJ=@v_?70s3HxQPk+{6Y+lM85f2NF^00*^OcM0~?JOZfR9ZPYF+# zYSs}(_BUYV8{n@2a1hD^SV41bwmi2uztR;PeBgF1F-`9>`zoNss-@3LaF2sjl~>OaaVmp7PNp+UT`6@}gR%uzqHDVeEZ14{Yt?n%JeQm+t(1_u zSc}oj^{b;+rlS|ME%+LjzSI&xu0Bblxo$MJ-J$kJ?Qu_XUXh}*@*-x@ny|}wVM%Lg z3tNB`yvr*}N?ClGL;H2cglcvErIccU3(eP7>@~4nOIcI~-`P8tSQnx=jI&{9)!1}l z;gQ%_h>ZlPSV@o@Azq1R$C6ja5!^ZGh;YRhhxs58qJWo9@Bceac&yy(pET1hnn`~7@}2L0&dfPKYs$ih7m2}R!25!(hxqA(!UIw; zK4+~Jowy3=RNC6nE=ncU{LH5?*9@W24lacJlvCZXB$CYtE@>c+~H zkV=(5I&gb{xn2!~f&fs2NQgAL6`p|kyt6kpWk}iVlqIp(H;ig`{_U9yxs1jzu^ETM z7~)Rg8C-NueqTYP&U8l{DY=Y47cR zOR@U%$KQV{mkRF|4)z9Y^t3K`@p>duY&QLUFeh6VoV`a`$U@)(z!-N*5Cj<11$EZW&hJLX83TO{lJYP74rlDZQPkm@t<=U^I)x@|UnHHkdQlh?!ltZwl92rE;;^ zZuIappj4dhld1}kttYYV-j|KF1Kus zWBnzttD^00%LFK(wrwNragFub6xiV8QE2rm<`&fcR4SLFcdtLxVuN!Aal-g6dE4%k zARZ}|xeo;K{0yf7@9aua%2j5o)CPcIOc6uLHFJOcgtB5owlcNAwyAHc0QB0Dts?c@ zUemG~j_E&W7R%+x-IO4FJl8e&*2Blmp1S#RA|)geVrxvP)NHdYuxi~g&Etn?QdNK8ZDKZ?QFLU?zh30G|t9G>a_X4zk}Ygw<^$7K!GIn(Io$>(d4ODJQ2XSd%jpK zm7>ptl$a3GyB}5-%p4>Q*p#VL^B{yQMuFCM^#l#+N!Ne z5_PrJWB=@Iy+t)H`g1lX`{bm($KE5I?0c(JEYm#t{F}j!xtsbob0{xu@0TB_*>G7w0ICn zr#VoBktqHZ~XxhiKD*lcG|b;H*|Ny3P^8ceV`sfBRfrhwZ!T+MFZ!F1Bt{q$8d9i6o?~ zODj^POr}&ivSa^R^YFIq7o0giLBKCycH_aU`F6)O6JX%nPTwh~Q`eq6*0iE#Srj2^ z*_hN3%*b83zfafy60@Cp3{J({RlSaEn&E?mrxRNC9GQ7#+f=s! z0KBf-9Ny_v2VbE%aB|Di)5kNJ^t&C`4D(>t7zYUWUFtbxt+Oq=!@O7BU)}>d*R72o zFF)3jQD_lLe4is&xzyJYC1-c{8TX$RU>&>P$%)ufpez0XSAukmh!xcekg`s$c<>-q zI#zn^JU0zzF}V60)o$_gY}PQH>b2M9&8fRZa#OauglPb zeQ@pMm&=!vNgos4CluQjLMV!pfkmxK+35bi^k&=k>9h02?l+u+m0agG;(h2|Jslc-llvtEwn~*w3bx7qnvZACG<8}AGeaDVvcHbKd2>3G^ zSFPULUn-?Pmo^-_`mLZr??uNH`2=I&yajlrF{DtUxMy#Nu}z=3y7qbUA;5`)hibMR zhXL@@uKyV0-2&A@t@!xyrBnMJl&^o@Gx$&5_q6?D=ji5grd-~=?dlg;ur(_V0wjh! zA=JV^C1m+DDkOsgr<%O9ZQFg!0}pD(#PSz4Dr_EyS5$`)VIAv);4n-SFP~YtC7sH= z7&*MfpH;gd*FHbkmD#)hVxb6xjc9~`t?_{=JS+@ip_cTicXxG<=7m9& zPX+Z8IC*GSAXuGCrZDHgR$r%jyk-fctis2Kx4HvZ|B~8uC@o)m^>Hy-O!&TKA?$&n zkP2Xc54w~!=z2?^NafyL*L0V9cbYrugHBBUj`xVyZmGFR&kvk#>1J*Z~i zNTz}?IAdJ$gkqd2!Gw(%LzE!O5s4C7q4%T~e_P{+z=DNDKrG**p=U`d5yg^vp`;Zn zsU=8gd0a9s4s0FPJePWR9eH5=+O^Kks&kC-iblNqTh2&Pw*^(4384f+D8N|fewZu_ zg2ejQ)ov;ztz;NQl7yj;A`(!H!XQu_$sqY9h_IrH*}_%1{L&_YLDvO?%R5Z-t+ClW z_qERbL?HKUZ!nt+!E9S`uoh^5A|DaIHe*_gf1`E_Vq+}{&T@t$EGhMnRjJ4z2w_W8 zp+qjs7as22^&S3wY1?+}^j-I=RcCE>#|39)g(lU7v_8;?=qK(9D8-*pPdiy)P3lIblG`+?%ea| zYoD3dopYt!tKgFicfNmNi(EWE=E4hC6(r|PYtanqJlmt57YOVrr2^tfrG(eG9C##X zu&1t@%L$RIvpj!wUA z8i>Pqot#_+Cnp6L2XPcZy1ar|9MnY+7eNvK1E)@Tr#2KsXq1*>)uUCozT7L##ok?o zhA6ofP4E|b*9tAfG?uf$#}>TIR&1A!yslP8}i7w-EzW(x#9VEvx18k%Tn=-$VV zkOtUr0b2!w3t>h?#8AZl^Az*(6KCGlD;4j~yx};`#2gN1_gv=%7KVzecIRakN{f*4 zeaI>yH;-o4OGhvGTU)(quWI)-q?V*(sVesSMv|wMUQ3hLEt=lBB$KZ9TyHr>)f7o%) zPYeU<3P)*P10*7vE)nA5#{c=6-E-_>r_u4e3i!I2+UksELwDqwMeBZ9FSP$;^Ajro z_@M#_Ss$?ejoB@!wN|kbGKs(0zLo%0QpQXW#t;oC$B0MZYZ&Ej?8~fNhcCVvPo3vo zFn0WWZaPliF^8_}yzb`*f@yg0uWv6HgNI)xa=pO%Ck(C<=-60l#uD3(wXP~c7!NoX z0&^6=N`zcc90F#qt@=Rn@r!3(*1v(Tl{B!m?Mc7yIA+nEHpY{YWr$=)F7rhR1P}(v zt{YhY#;jsW6G>#xhP*B`OCk|Pf+NN;ju1rxa*HAgoGq*rvqw&xe~;t1JA31$s?GBb z*g7&@cbKo4n<`>)!UlIAgR6q&))B0KYU8r66GbFj?8Guw4E%&}Qi_lT003LtoIZei zwD~=XZmeo+yZ2Pq3KYCF-R&11^p= z@H%s+=G`}wrbJ{()Mh71#2SP3Zy3m>l1n?0N-N1Q;z6?oSxr-G(H5m4EO>~&;}VKi zfY}3w+9z>vp#d)hVuu`)vG_aaH%3b=WKMnSu&c31;<3O;bz2iD=w+o4#oBb36 z5ZCF*Gu?zjZIR0S>_%pHY2$k8D^n7Sz_K8tCDeXM+dO<#LSg%h6`~dnVG1N@T7v&e z%wEd1!k{^zfz_1BTW{!$!B%g)J^2b87!9Y>>100X1SgT7s0z$o>^lAA=Gp_cC1(h=*5Tmf8z&LGJJ>$|K^~s`z9*OWz5MFUr?>Bi?_PGBB)#psD5?>n+q{o_ zz7~ez&;t#h8l$jwGPCC&xq2YetXYQT+0F3j(`xmNGf8dj#an|p#I*pvI*kwW4iuB> z+q3_7xB8y;pLzHG-S%+UHQA zvqp;$kmGJY>lLsN4C~&TcvAS1SErTcwcw0r@wngk zShAUA1M9b#g}^pL-zH7Q#z^&j#r9F8BTVfkR&qF<=e35goTu7c|GN)0mokj4m0%~0 zXJ8j4Hc_l;HJ&uU*Iw`8d_EscJ``s0tk9mkKo^&#TYXm-EoAzTQObxa@^u~g2t#T) zJz|rE!I_?i4dCJC=B8(_pZ{YR>|V?0iCcnU;E@$239^x?SYCfNaMHN;CtHIS_zHN9 zTkQc1v@O35okiFtq5_u+5FkY55ap@pi)O?}x0D1c*qB0KpYR}>Ul+B0Vmr}Z@+%mJ|As}sis_=ROPbov@*2thpE&?!V#Qgu$snYvCZ zrkhmkMU+fSf-s8(L37fPr&M*jRs{{THb!aXQu|P9l_-vJhHvLzMGH zE?1U0H_+PmNABp9`|KzkGfrrZ%XvdGo6*<{d5m9~L7 z_^`M;X6xDo=m6LY6RfvJEvsTK1!u8d2HPx|$S}p;sRy!I zWL55Yxu~_B`OP@~(q6&W3#)~I&+MGL%GWR$#udC151^wsswhqlii;rP9jJpiI7o&Z zAb})=HY7?4HA|re3ns`%$)FuvKCFWjhb~?IE)F6dF2K5}poj-NK6Gf;hw$t3=1txY zoxQxZWrQU6K!%|~!m?~Bnw-6Rr!F3BZ{u5!LqnZTDON}Coj9^@&le)V!NYrVwS~B% zEL+>Sr@}qGwGvu|HrOo|gSt__ezN^&%~{*)a=rf7y1HujUcr`zZB<4#l@T#eN)si} z)lZA<{=tKx8E%c9>A(##6}_p+~EZpKsl5a4pj`E*;_-6`ysiv zffA!7=MT1vCz}-m4~tjVey1b2KSR4OEtLd-(_DdUqYZ74LaDkhH?KFh?%WAOP2WbX zp@zT+Dx|5_f%JQiAGvVw!oh+g3e50u!aPfMxdC=E)XB{F5IcEZhePIM- zph6Y`$Oy?JBL<8Ex(SqEhLeQ@XcrdA>a?rx+_~HLA;l14)WmmpH}_w?Pg#HBZs0eS zwypwAW?M-x+3AU-(GGWSJ=ngxUEcEZ5OsX(Qlt!MQ zn^(`S{GHkAv(8@D`EAfSYig%Cxv?z!{=w^F#y)5_d7FuKZH7qlR-#5B0bt806%D0I zT7VdVP_?q*%Rq8UR;JkD4i^RXowt+E%#V2U>TfDqzZSDZ+dR!a#T3I>-z_$q9@k|m zy5~A*m~&JWP@E7a=pc}4kVHTc4h&R;Li7d@f`|hKMLkbb^uhOakNr3&FLjlm~i5NBM< zFaYI{;cpiHCNRdE0dg*>qIm(_t?#$h=(SCw?h3rJV2*ER8{O4^3#=dO)KwklZkoqU zS8i5c%YL*y*4;FY#D=XmkQnYj%LH)?02~gSJH`Qp1XY64g>%c_K$xseI&|e)7vRoL zAqRba$G@%fSGA7X7hQk%_3NVOYVS+$leU_!&6*5uN)8#5ZBz_6ASCA;azYS-Rt@ki zg2NWz(=;t}SC(~Ibl63$5C8FPmhXqb^)5#jaJ~I{Ex3xZ!+2h8$}}h_g@Be>HZ;72 z6#y#>AY3^skuVKF#0WxFBQ()5d5_nWb?c6c>EeMM|Mh+*&wEpPyxHCq{R-Gdr-`hN zF=1sxl&mBoK+#qRLl9#CEN|Fg8>nbmsTg3a1;#M9enQ$RgWk}kp#-5wh=EF&1tl%mJln2V^8o%Qv(*=zEuO7y z=m*8?xpUn-*@h5Cl_3BK3joiGkyaScK+>|MWdMRWm@RT!Q1piAlv5hL@B6>3&GI8) zP!xBc6}ZNIpJLL%2a8Y!+(<=f%WX>_uWVxlga9!D*oYt$l0cxRDMvqfU;Kq_mLK5k z)dvqYcgLa_Lz?3HyeF)@$%$&6lI?r4I>6W#M*<)vq{?&Oqrx``d`mhpVPr> z#q078F6gw_X<=?KR>8%^t%@wbITvNMu!hKiTSkCTJkw>1!e*Y{%31#_yMf=LW7{RJ zYoC^w$6%3cBtVG5)x#{Hg6IVTh9XEcM{gQwXk!R^y95^f-hZ`d{aVa+xW1EO4wDV4 zB?JgD7*?qkvc|$nIykTvNl2x0j3Q!MXoLL^)~}d7jcYf(H8D~c+?$pKL(px>Z3`eb z04RzS6_AgFT6Pn#iZAg$Sl_j8#;6ShF%&(Fag#E2asU@@LaN;=b=Wf7sgPKhfzhBM zC@eFL8^MrnA*9&Khe*Ab@CC9*uyJGXyi(;y2>lQLJZt;ShtJi?3Yf_t`F+$hY!+Q2Ndsx=U+bjTiAy7djLji>7k%k`$9&--f<*BNA3Hy&ZrHH|4 zG5H&9cB?O#zI1_OOf0Ce%mDfQxdtp3vU%(iY6yji3iISS61XLv#z|!zI_sZqza@B+ zyu9st5-h+`H7QUKx9}3w@oU@EO}&cEzG?fu!!bLO->%zkcg;i9^j`S~=WKMnDi1f= P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/config/docs.config.ts b/src/config/docs.config.ts new file mode 100644 index 0000000..d22d292 --- /dev/null +++ b/src/config/docs.config.ts @@ -0,0 +1,67 @@ +export type DocLink = { + label: string + href: string +} + +export type DocCard = { + title: string + label: string + mark: string + description: string + href?: string + status: string + accent: string + links: DocLink[] +} + +export const docs: DocCard[] = [ + { + title: 'SLM Design', + label: 'Архитектура', + mark: 'SLM', + description: 'Архитектура frontend-приложений, где слои задают направление зависимостей, модули становятся границами ответственности, а явный DI через фабрики удерживает домены изолированными и предсказуемыми.', + href: '/slm-design/', + status: 'Доступно', + accent: 'violet', + links: [ + { label: 'llms.txt', href: '/slm-design/llms.txt' }, + { label: 'llms-full.txt', href: '/slm-design/llms-full.txt' }, + ], + }, + { + title: 'NextJS Style Guide', + label: 'Стиль проекта', + mark: 'NX', + description: 'Правила организации Next.js-приложений, роутинга, серверных границ и проектных соглашений.', + status: 'Скоро', + accent: 'blue', + links: [ + { label: 'llms.txt', href: '/nextjs-style-guide/llms.txt' }, + { label: 'llms-full.txt', href: '/nextjs-style-guide/llms-full.txt' }, + ], + }, + { + title: 'React Style Guide', + label: 'Стиль кода', + mark: 'RE', + description: 'Практики написания React-компонентов, хуков, состояния и клиентского UI-кода.', + status: 'Скоро', + accent: 'cyan', + links: [ + { label: 'llms.txt', href: '/react-style-guide/llms.txt' }, + { label: 'llms-full.txt', href: '/react-style-guide/llms-full.txt' }, + ], + }, + { + title: 'Figma Adaptive Standards', + label: 'Макеты', + mark: 'FG', + description: 'Стандарты и требования к подготовке адаптивных макетов в Figma: брейкпоинты, ресайз в диапазоне, Auto Layout/Constraints, компоненты, сетка, типографика, состояния UI, A11y и передача в разработку.', + status: 'Скоро', + accent: 'pink', + links: [ + { label: 'llms.txt', href: '/figma-adaptive-standards/llms.txt' }, + { label: 'llms-full.txt', href: '/figma-adaptive-standards/llms-full.txt' }, + ], + }, +] diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..3b1cd97 --- /dev/null +++ b/src/index.css @@ -0,0 +1,83 @@ +:root { + color-scheme: light; + --sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --page-bg: #eef1f7; + --surface-soft: #f9fbff; + --text-primary: #101522; + --text-secondary: #424b60; + --text-muted: #7d879b; + --border-soft: #cfd6e5; + --accent-cool: #5866dc; + --grid-line: rgb(196 204 222 / 0.48); + --hero-glow: rgb(88 102 220 / 0.12); + --focus-ring: rgba(102, 119, 255, 0.56); + --shadow-subtle: rgba(25, 31, 54, 0.1); + + font-family: var(--sans); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +:root[data-theme='dark'] { + color-scheme: dark; + --page-bg: #1b1c21; + --surface-soft: #202229; + --text-primary: #f1f3f8; + --text-secondary: #a9afbf; + --text-muted: #747c90; + --border-soft: #343741; + --accent-cool: #8492ff; + --grid-line: rgb(72 76 92 / 0.34); + --hero-glow: rgb(132 146 255 / 0.18); + --focus-ring: rgba(132, 146, 255, 0.66); + --shadow-subtle: rgba(0, 0, 0, 0.24); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + color-scheme: dark; + --page-bg: #1b1c21; + --surface-soft: #202229; + --text-primary: #f1f3f8; + --text-secondary: #a9afbf; + --text-muted: #747c90; + --border-soft: #343741; + --accent-cool: #8492ff; + --grid-line: rgb(72 76 92 / 0.34); + --hero-glow: rgb(132 146 255 / 0.18); + --focus-ring: rgba(132, 146, 255, 0.66); + --shadow-subtle: rgba(0, 0, 0, 0.24); + } +} + +* { + box-sizing: border-box; +} + +html { + min-width: 320px; + background: var(--page-bg); +} + +body { + min-width: 320px; + min-height: 100svh; + margin: 0; + background: var(--page-bg); + color: var(--text-primary); +} + +button, +a { + font: inherit; +} + +p { + margin: 0; +} + +#root { + min-height: 100svh; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..7f42e5f --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..1dd8f4d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,63 @@ +import { existsSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { defineConfig, type Plugin } from 'vite' +import react from '@vitejs/plugin-react' + +function serveBuiltDocs(): Plugin { + return { + name: 'serve-built-docs', + configureServer(server) { + const publicDir = resolve('public') + + server.middlewares.use((req, res, next) => { + const url = req.url?.split('?')[0] + if (!url) return next() + + const filePath = resolve(publicDir, url.slice(1)) + + if ((url.endsWith('.txt') || url.endsWith('.md')) && existsSync(filePath)) { + res.setHeader( + 'Content-Type', + url.endsWith('.md') + ? 'text/markdown; charset=utf-8' + : 'text/plain; charset=utf-8', + ) + res.end(readFileSync(filePath)) + return + } + + const [, site] = url.split('/') + if (!site || site.includes('.')) return next() + + const siteDir = resolve(publicDir, site) + if (!existsSync(resolve(siteDir, 'index.html'))) return next() + + if (url === `/${site}`) { + req.url = `/${site}/index.html` + return next() + } + + if (url.endsWith('/')) { + if (existsSync(resolve(filePath, 'index.html'))) { + req.url = `${url}index.html` + return next() + } + } + + if (!url.split('/').pop()?.includes('.')) { + if (existsSync(`${filePath}.html`)) { + req.url = `${url}.html` + return next() + } + } + + next() + }) + }, + } +} + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), serveBuiltDocs()], +})