diff --git a/kcal/.gitignore b/kcal/.gitignore
new file mode 100644
index 0000000..a4d699a
--- /dev/null
+++ b/kcal/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+dist
+*.local
+.DS_Store
diff --git a/kcal/README.md b/kcal/README.md
new file mode 100644
index 0000000..f4a259d
--- /dev/null
+++ b/kcal/README.md
@@ -0,0 +1,86 @@
+# Ккал — локальный дневник питания
+
+Считаете калории — приложение делает это быстрым: запись за 2–3 касания,
+остаток на день всегда перед глазами, все данные только в вашем браузере
+(IndexedDB, без бэкенда и аккаунтов).
+
+## Стек
+
+- **Vue 3.6.0-rc.2, Vapor mode** — без virtual DOM; компоненты написаны на TSX
+ через **vue-jsx-vapor** (компилятор на Rust/Oxc), `createVaporApp` в `main.ts`.
+- **vue-sync-engine** (`../vue-sync-engine/lib`, подключён как `file:`-зависимость) —
+ нормализованный entity-кэш: запросы, мутации с optimistic-патчами,
+ персистентность в IndexedDB.
+- **Tailwind CSS 4** как Vite-плагин, дизайн-токены в `src/app.css` (`@theme`).
+- **@robonen/stdlib** (`clamp`, `groupBy`), **@robonen/vue** (`useCloseWatcher` —
+ закрытие шторок по Esc и жесту «назад» на Android, с фолбэком на keydown),
+ **@robonen/platform** (`focus`), **@robonen/tsconfig**, **@robonen/eslint**.
+- **date-fns** (+ `ru`-локаль: родительный падеж месяцев из коробки).
+- **lucide** — данные иконок (пары «тег + атрибуты»); рендер своим Vapor-компонентом
+ через `v-html`, т.к. `lucide-vue-next` построен на vdom и в чистом Vapor не работает.
+- Шрифты: Golos Text (UI, кириллица) + Spectral (display-цифры и заголовки).
+
+```bash
+pnpm install
+pnpm dev # vite
+pnpm test # vitest, доменные расчёты
+pnpm typecheck # tsc --noEmit
+pnpm lint # eslint (@robonen/eslint, eslint 10)
+pnpm build # tsc + vite build
+```
+
+> vue-sync-engine должен быть собран: `cd ../vue-sync-engine && pnpm --filter vue-sync-engine build`.
+
+## Как устроен local-first слой
+
+Бэкенда нет, источник истины — IndexedDB (`kcal`):
+
+- `defineEntity({ storage: idbStore(...) })` — каждая сущность в своём object store;
+ `EntityDef.storage` используется и как прямой доступ к idb.
+- **Запросы** читают из этих же сторов (`readAll` + фильтр), нормализуют сущности
+ в Mirror и возвращают списки id. Снапшоты запросов персистятся через
+ `indexedDBAdapter` — после перезагрузки данные всплывают мгновенно.
+- **Мутации** пишут в idb внутри `fetch` (запись await-ится до `invalidate`,
+ поэтому рефетч всегда видит свежие данные), `optimistic` даёт мгновенный UI,
+ `invalidate` по тегам (`entries`, `foods`, `weights`, `profile`) обновляет
+ списки и статистику.
+- Записи дневника хранят **снапшот** имени и нутриентов — правка или удаление
+ продукта не переписывает историю.
+
+Структура: `src/domain` (типы, расчёты Миффлина—Сан Жеора, даты — без Vue),
+`src/data` (дефы движка, сид-каталог ~60 продуктов, бэкап), `src/screens` +
+`src/components` (TSX Vapor), `src/ui` (состояние навигации, иконки).
+
+## Что уже умеет
+
+- Онбординг: BMR/TDEE по Миффлину—Сан Жеору, цель (похудение −15% / поддержание /
+ набор +10%), белок 1.6–1.8 г/кг, предупреждение о слишком низкой цели.
+- Дневник: кольцо остатка, полосы Б/Ж/У, приёмы пищи, листание дней.
+- Добавление: поиск по каталогу, «Недавние» (частота + последняя порция),
+ граммы/штуки, живой пересчёт, быстрая запись «только калории», свой продукт.
+- Штрихкоды: сканер камерой (BarcodeDetector, Chrome/Android) и ручной ввод
+ цифр — КБЖУ подтягиваются из Open Food Facts (barcode-API отдаёт CORS `*`,
+ запрос идёт прямо из браузера). Повторный скан того же товара сразу открывает
+ выбор порции (дедупликация по `Food.barcode`). Текстовый поиск OFF из
+ браузера невозможен: legacy-эндпоинт отключён, у search-a-licious нет CORS.
+- Справка в профиле: 7 практических вопросов — откуда брать КБЖУ, как писать
+ ресторанную еду, сколько точности достаточно, как считаются цели.
+- Статистика: калории по дням (7/14/30) с целью-линией и выбором дня, средние,
+ «дней в цели», журнал веса с трендом за неделю.
+- Каталог: поиск, категории, правка/удаление (сид-набор редактируется как свой).
+- Профиль: пересчёт целей, ручная правка, экспорт/импорт JSON, полный сброс.
+
+## Дорожная карта
+
+- **Кросс-таб синхронизация** — перевести движок в режим SharedWorker
+ (`bootstrapWorker` + `createSharedWorkerClientTransport`), дефы уже
+ собираются плагином в `virtual:sync-engine-registry`.
+- **PWA** — manifest + service worker, чтобы поставить на домашний экран.
+- Порции-пресеты у продукта («стакан», «ложка»), копирование вчерашнего дня,
+ конструктор рецептов (сумма ингредиентов ÷ готовый вес = свой продукт
+ «на 100 г») — главный недостающий кусок для домашней готовки.
+- Недельный отчёт: средний дефицит vs фактическое изменение веса
+ (замыкает петлю «оценка калорий → реальность»).
+- A11y: связать `label`/`id` у полей, `aria-live` на кольце, фокус-ловушка в шторках.
+- Индекс по дате для записей (`IDBKeyRange` вместо `readAll`+фильтра), когда
+ дневник разрастётся.
diff --git a/kcal/eslint.config.ts b/kcal/eslint.config.ts
new file mode 100644
index 0000000..60fa8f0
--- /dev/null
+++ b/kcal/eslint.config.ts
@@ -0,0 +1,16 @@
+import { base, compose, imports, stylistic, typescript, vitest } from '@robonen/eslint';
+
+export default compose(
+ base,
+ typescript,
+ imports,
+ stylistic,
+ vitest,
+ {
+ name: 'kcal/overrides',
+ rules: {
+ // Дефы движка и доменные расчёты плотно работают с числовыми литералами.
+ 'unicorn/no-zero-fractions': 'off',
+ },
+ },
+);
diff --git a/kcal/index.html b/kcal/index.html
new file mode 100644
index 0000000..3dfabb1
--- /dev/null
+++ b/kcal/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Ккал — дневник питания
+
+
+
+
+
+
diff --git a/kcal/package.json b/kcal/package.json
new file mode 100644
index 0000000..584cd83
--- /dev/null
+++ b/kcal/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "kcal",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "description": "Локальный дневник питания: калории, БЖУ, вес. Vue Vapor + vue-sync-engine + IndexedDB, без бэкенда.",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc --noEmit && vite build",
+ "preview": "vite preview",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix"
+ },
+ "dependencies": {
+ "@fontsource-variable/golos-text": "^5.3.0",
+ "@fontsource/spectral": "^5.3.0",
+ "@robonen/platform": "^0.0.5",
+ "@robonen/stdlib": "^0.0.12",
+ "@robonen/vue": "^0.2.0",
+ "date-fns": "^4.4.0",
+ "lucide": "^1.28.0",
+ "vue": "3.6.0-rc.2",
+ "vue-sync-engine": "file:../vue-sync-engine/lib"
+ },
+ "devDependencies": {
+ "@robonen/eslint": "^0.0.1",
+ "@robonen/tsconfig": "^0.1.0",
+ "@tailwindcss/vite": "^4.3.3",
+ "@types/node": "^25.9.1",
+ "eslint": "^10.8.0",
+ "jiti": "^2.7.0",
+ "tailwindcss": "^4.3.3",
+ "typescript": "~6.0.3",
+ "vite": "^8.2.1",
+ "vitest": "^4.1.8",
+ "vue-jsx-vapor": "^3.2.19"
+ }
+}
diff --git a/kcal/pnpm-lock.yaml b/kcal/pnpm-lock.yaml
new file mode 100644
index 0000000..8717715
--- /dev/null
+++ b/kcal/pnpm-lock.yaml
@@ -0,0 +1,3260 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@fontsource-variable/golos-text':
+ specifier: ^5.3.0
+ version: 5.3.0
+ '@fontsource/spectral':
+ specifier: ^5.3.0
+ version: 5.3.0
+ '@robonen/platform':
+ specifier: ^0.0.5
+ version: 0.0.5
+ '@robonen/stdlib':
+ specifier: ^0.0.12
+ version: 0.0.12
+ '@robonen/vue':
+ specifier: ^0.2.0
+ version: 0.2.0(vue@3.6.0-rc.2(typescript@6.0.3))
+ date-fns:
+ specifier: ^4.4.0
+ version: 4.4.0
+ lucide:
+ specifier: ^1.28.0
+ version: 1.28.0
+ vue:
+ specifier: 3.6.0-rc.2
+ version: 3.6.0-rc.2(typescript@6.0.3)
+ vue-sync-engine:
+ specifier: file:../vue-sync-engine/lib
+ version: file:../vue-sync-engine/lib(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))(vue@3.6.0-rc.2(typescript@6.0.3))
+ devDependencies:
+ '@robonen/eslint':
+ specifier: ^0.0.1
+ version: 0.0.1(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)(vitest@4.1.10(@types/node@25.9.5)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0)))
+ '@robonen/tsconfig':
+ specifier: ^0.1.0
+ version: 0.1.0
+ '@tailwindcss/vite':
+ specifier: ^4.3.3
+ version: 4.3.3(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))
+ '@types/node':
+ specifier: ^25.9.1
+ version: 25.9.5
+ eslint:
+ specifier: ^10.8.0
+ version: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ jiti:
+ specifier: ^2.7.0
+ version: 2.7.0
+ tailwindcss:
+ specifier: ^4.3.3
+ version: 4.3.3
+ typescript:
+ specifier: ~6.0.3
+ version: 6.0.3
+ vite:
+ specifier: ^8.2.1
+ version: 8.2.1(@types/node@25.9.5)(jiti@2.7.0)
+ vitest:
+ specifier: ^4.1.8
+ version: 4.1.10(@types/node@25.9.5)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))
+ vue-jsx-vapor:
+ specifier: ^3.2.19
+ version: 3.2.19(csstype@3.2.3)(rolldown@1.2.3)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))(vue@3.6.0-rc.2(typescript@6.0.3))
+
+packages:
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.8':
+ resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@emnapi/core@1.10.0':
+ resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
+
+ '@emnapi/runtime@1.10.0':
+ resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
+
+ '@emnapi/wasi-threads@1.2.1':
+ resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
+
+ '@eslint-community/eslint-utils@4.10.1':
+ resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.23.5':
+ resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/config-helpers@0.7.0':
+ resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/core@1.2.1':
+ resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/js@10.0.1':
+ resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ peerDependencies:
+ eslint: ^10.0.0
+ peerDependenciesMeta:
+ eslint:
+ optional: true
+
+ '@eslint/object-schema@3.0.5':
+ resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/plugin-kit@0.7.2':
+ resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@fontsource-variable/golos-text@5.3.0':
+ resolution: {integrity: sha512-D+Evz5yqXYh6tsalc537FS+cPDWn2FTdO8j/K1ZZuFAvA7ib6WnusTiihciq7mMnm6GBo6XTlh0JXPrhBsam2g==}
+
+ '@fontsource/spectral@5.3.0':
+ resolution: {integrity: sha512-2TL5WWGOZj4SIsBmPFEzKeA60tXXd1Qp1OqaaVEWkJL8IfSGLk6YmiusmC441iFkdJDIuFelu1r8lhCmQUU3mw==}
+
+ '@humanfs/core@0.19.2':
+ resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.8':
+ resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/types@0.15.0':
+ resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/wasm-runtime@1.2.2':
+ resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3
+ '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3
+
+ '@oxc-project/types@0.143.0':
+ resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==}
+
+ '@robonen/eslint@0.0.1':
+ resolution: {integrity: sha512-vLdPIRurtXOnhPl/1AOepvJ5qDoPM5meUuyEDPb3Bo97F5Kl1PsdQoBiWrcdtHKN9f+HmreHcaP8C7UQkC7Rsw==}
+ engines: {node: '>=24.16.0'}
+ peerDependencies:
+ eslint: '>=9.39.4'
+
+ '@robonen/platform@0.0.5':
+ resolution: {integrity: sha512-RrcW5V1oAUqCDVjjqQsc8ov0L+XUfcaaVKkD5USonDWE2M6VG4dQP+r1+S+OQOD6zKCrrCsH4Sat6Ye94D/RVQ==}
+ engines: {node: '>=24.16.0'}
+
+ '@robonen/stdlib@0.0.12':
+ resolution: {integrity: sha512-eGL24DggdqCKXpU8dGKhos2RVOIGGbZb9livCuryk2F2ufMfBx7O0ApkQ9f7kwNYPV8pGxg4tomPxXgEDoKVog==}
+ engines: {node: '>=24.16.0'}
+
+ '@robonen/tsconfig@0.1.0':
+ resolution: {integrity: sha512-wM8KkVYvuELxBiruiUs1lbZAckp85PdxQrrAcGWo4CUpK6fO5M2+G+MC9BB2sVnOv+hBLiUvfd88GPx3viKUMw==}
+ engines: {node: '>=24.16.0'}
+
+ '@robonen/vue@0.2.0':
+ resolution: {integrity: sha512-f/JFdsE1wlY+Uw0r7azTbGB7xtHt5hrCevGIp7ccvx7RXQaaGjM2PMQcTh11O+kqFOo9NW/BjqHqOYgM9YvshA==}
+ engines: {node: '>=24.16.0'}
+ peerDependencies:
+ vue: ^3.5
+
+ '@rolldown/binding-android-arm64@1.2.3':
+ resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.2.3':
+ resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.2.3':
+ resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.2.3':
+ resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.3':
+ resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.3':
+ resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.2.3':
+ resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.3':
+ resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.3':
+ resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.2.3':
+ resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.2.3':
+ resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.2.3':
+ resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.3':
+ resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.2.3':
+ resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@stylistic/eslint-plugin@5.10.0':
+ resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^9.0.0 || ^10.0.0
+
+ '@tailwindcss/node@4.3.3':
+ resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==}
+
+ '@tailwindcss/oxide-android-arm64@4.3.3':
+ resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.3':
+ resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.3.3':
+ resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.3':
+ resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3':
+ resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.3':
+ resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.3':
+ resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.3':
+ resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.3':
+ resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.3':
+ resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.3':
+ resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.3':
+ resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.3.3':
+ resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/vite@4.3.3':
+ resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
+ '@tybys/wasm-util@0.10.3':
+ resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/esrecurse@4.3.1':
+ resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/node@25.9.5':
+ resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==}
+
+ '@typescript-eslint/eslint-plugin@8.66.0':
+ resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.66.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/parser@8.66.0':
+ resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/project-service@8.66.0':
+ resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/scope-manager@8.66.0':
+ resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.66.0':
+ resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/type-utils@8.66.0':
+ resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/types@8.66.0':
+ resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.66.0':
+ resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/utils@8.66.0':
+ resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/visitor-keys@8.66.0':
+ resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@unrs/resolver-binding-android-arm-eabi@1.12.2':
+ resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==}
+ cpu: [arm]
+ os: [android]
+
+ '@unrs/resolver-binding-android-arm64@1.12.2':
+ resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==}
+ cpu: [arm64]
+ os: [android]
+
+ '@unrs/resolver-binding-darwin-arm64@1.12.2':
+ resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@unrs/resolver-binding-darwin-x64@1.12.2':
+ resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@unrs/resolver-binding-freebsd-x64@1.12.2':
+ resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2':
+ resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==}
+ cpu: [arm]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2':
+ resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==}
+ cpu: [arm]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-arm64-gnu@1.12.2':
+ resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@unrs/resolver-binding-linux-arm64-musl@1.12.2':
+ resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@unrs/resolver-binding-linux-loong64-gnu@1.12.2':
+ resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@unrs/resolver-binding-linux-loong64-musl@1.12.2':
+ resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [musl]
+
+ '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2':
+ resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2':
+ resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@unrs/resolver-binding-linux-riscv64-musl@1.12.2':
+ resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@unrs/resolver-binding-linux-s390x-gnu@1.12.2':
+ resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@unrs/resolver-binding-linux-x64-gnu@1.12.2':
+ resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@unrs/resolver-binding-linux-x64-musl@1.12.2':
+ resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@unrs/resolver-binding-openharmony-arm64@1.12.2':
+ resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@unrs/resolver-binding-wasm32-wasi@1.12.2':
+ resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@unrs/resolver-binding-win32-arm64-msvc@1.12.2':
+ resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@unrs/resolver-binding-win32-ia32-msvc@1.12.2':
+ resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@unrs/resolver-binding-win32-x64-msvc@1.12.2':
+ resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@vitest/eslint-plugin@1.6.26':
+ resolution: {integrity: sha512-2eawZk4MZvkEtMZFdScmCE0R1Rti85uRZpmbe9eAv5Q3blDZ5OxlrC9IHlSnxhDCqOO2xKNaD3oufl9BUlI0yA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@typescript-eslint/eslint-plugin': '*'
+ eslint: '>=8.57.0'
+ typescript: '>=5.0.0'
+ vitest: '*'
+ peerDependenciesMeta:
+ '@typescript-eslint/eslint-plugin':
+ optional: true
+ typescript:
+ optional: true
+ vitest:
+ optional: true
+
+ '@vitest/expect@4.1.10':
+ resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
+
+ '@vitest/mocker@4.1.10':
+ resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.1.10':
+ resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
+
+ '@vitest/runner@4.1.10':
+ resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
+
+ '@vitest/snapshot@4.1.10':
+ resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
+
+ '@vitest/spy@4.1.10':
+ resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
+
+ '@vitest/utils@4.1.10':
+ resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
+
+ '@vue-jsx-vapor/compiler-rs-android-arm-eabi@3.2.19':
+ resolution: {integrity: sha512-HefhdfW2cqQgmstyFySUtsf0ClWvfJacRto0nW0DzrNxojNmLlYKVoa4GnfcR5AitHlbPOnVorZT25rVCJ/MSA==}
+ cpu: [arm]
+ os: [android]
+
+ '@vue-jsx-vapor/compiler-rs-android-arm64@3.2.19':
+ resolution: {integrity: sha512-Nqz1Fbv9+MxisXhC7Do5zbiB8LQ5kwvLPyqqVpc/KVsKbaitjYGVjuve455wik6kNpfNXGPhLQNywAf9SjMbeg==}
+ cpu: [arm64]
+ os: [android]
+
+ '@vue-jsx-vapor/compiler-rs-darwin-arm64@3.2.19':
+ resolution: {integrity: sha512-MXZdt87BptqSCGT6EVXwyOMUauf13XCHE2ZoHc4TFUdbaKtFSEXO5By2SIr7Tzix+PwKEoup/mEYdUCdEaz/Bw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@vue-jsx-vapor/compiler-rs-darwin-x64@3.2.19':
+ resolution: {integrity: sha512-Ctvxsl52Vjttc28pJf3J14+TuHXoVaPd5mB0w52CPChsKYmBolDat5GXCP82Li8OOAOzX4o7OcMT6UbFcYxdug==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@vue-jsx-vapor/compiler-rs-freebsd-x64@3.2.19':
+ resolution: {integrity: sha512-pEqJMObI4zHdPiaXI6/4gVcO2kmM0Ay63gMsW3IUOZhIv4OVwmBpLYaA7a3Mwjyk9TvHK+oze0ohTm9wWQuuNw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@vue-jsx-vapor/compiler-rs-linux-arm-gnueabihf@3.2.19':
+ resolution: {integrity: sha512-bwpP8QAcdSmFVv8s+79nSdcYZcm8gwehAtYzAT78Iun/FT9pniSzeJpeHi7lccMKgkmhrAGGat7moADY4QrfYw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@vue-jsx-vapor/compiler-rs-linux-arm64-gnu@3.2.19':
+ resolution: {integrity: sha512-omJ7FlyqchUqpPESe673vjHufzYy7rrOQ/gRGcCBWGlQfTg2tzk8YBxvdFgmqSeCHpIPev7ZdZyWKsjrQGOMyw==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@vue-jsx-vapor/compiler-rs-linux-arm64-musl@3.2.19':
+ resolution: {integrity: sha512-CCzyY/LX6vmhsFhOh/bqaVzIot/Mky9lz3CMgvbzUktUebxG/Rfh9oo4jFcjOb1QFWfNpDPRUZGsBaBXQ+gP2w==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@vue-jsx-vapor/compiler-rs-linux-x64-gnu@3.2.19':
+ resolution: {integrity: sha512-BA/KIwyzkT+jUSYA+t7pZMhZe8zShEDgIgjw94WwBbPLtzWMof+YLKY8FqNjN4nTXTwJn5yBuVtWD2LCg/6sWg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@vue-jsx-vapor/compiler-rs-linux-x64-musl@3.2.19':
+ resolution: {integrity: sha512-iYX0o9lnrTk19g2NAh0+EGflWk02LmKhNB5lwOXzXRVItyBuwqSsyy7fxad2rDHMdUT+gvp5HVA0rS28A11O2g==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@vue-jsx-vapor/compiler-rs-wasm32-wasi@3.2.19':
+ resolution: {integrity: sha512-0/maDIH/KMChDeUJhNik5HFahtJo8fXfYZT2gRtavTqhjAFC9f4WkVSwC87SC8zCi/mVT1cy5f6COCLwN3xTEg==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@vue-jsx-vapor/compiler-rs-win32-arm64-msvc@3.2.19':
+ resolution: {integrity: sha512-sDpi5f+b3qFx4zDOHZdsujPkd1FDT53ALdaPdgfYKwA1DNkR4ovY/TI3SsvSmPmEAmf6GsB7jGinShpX5HyaDg==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@vue-jsx-vapor/compiler-rs-win32-ia32-msvc@3.2.19':
+ resolution: {integrity: sha512-d4dxiaLsWDyM2zZjsP/LRDeJHoOy8/etrX1723Hlh2k8gzIITn77yzespEOK73dvdfusGbfOV0S/dkzqM0k09w==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@vue-jsx-vapor/compiler-rs-win32-x64-msvc@3.2.19':
+ resolution: {integrity: sha512-puiHMm14vhtar1ozcbQxSSJzNiuzcPT7dZQbY+ibWYuw2vchSHRXFS30gSvaIolz8RuHflrZ4uGHMTRdya+Pcw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@vue-jsx-vapor/compiler-rs@3.2.19':
+ resolution: {integrity: sha512-T1uviby19i2LH0T3D/HfpRTkS6xOpsvenknLnUU8qziZ71SLaLWqipbG4mCV1U8gxWvPEEkcgTij1eSXLR7M+g==}
+
+ '@vue-jsx-vapor/macros@3.2.19':
+ resolution: {integrity: sha512-4xEjB8uQ+MnD4bQwdchRumm0N1Ru2Q7nkz06DFsOXcCtx6oJdWnI4WEkOqXuXDdGHGcLl0pffcemtCMNhWmxXA==}
+ peerDependencies:
+ '@nuxt/kit': ^3
+ '@nuxt/schema': ^3
+ esbuild: '*'
+ rollup: ^3
+ vite: '>=3'
+ vue: '>=3'
+ webpack: ^4 || ^5
+ peerDependenciesMeta:
+ '@nuxt/kit':
+ optional: true
+ '@nuxt/schema':
+ optional: true
+ esbuild:
+ optional: true
+ rollup:
+ optional: true
+ vite:
+ optional: true
+ webpack:
+ optional: true
+
+ '@vue-jsx-vapor/runtime@3.2.19':
+ resolution: {integrity: sha512-pnULs33aW5G7tmKbveZjUorqChgP4HKrralQFIus//BPR5uZKJr2NVJtPxERzkCuc2BdHMG1SmN0TJtuFbACAQ==}
+ peerDependencies:
+ csstype: ^3.2.3
+ vue: 3.6.0-rc.1
+
+ '@vue/compiler-core@3.6.0-rc.2':
+ resolution: {integrity: sha512-Xx79h4EpJktQBShelXNKaRuvn9N6YsYL1j+RXdvPKzI+8tohYziBJbxOFGAkdcOKBwKc2SIWV4DOlTAMhE96yQ==}
+
+ '@vue/compiler-dom@3.6.0-rc.2':
+ resolution: {integrity: sha512-dRTdTdi/KXzHYIkHtyjGxepTIxnsZ/hB3v1osnS8c4MeycAyzkSoZW6OZkXXN4oVVVU6NKXds1XtKAEKVVw7nA==}
+
+ '@vue/compiler-sfc@3.6.0-rc.2':
+ resolution: {integrity: sha512-EZWTNh0UwdxvrBJkFc6+dqfwuUSdTVHDIohQcGDPCqGaYTHOfUL20b+grEAzlyxB741KrVBUb7qqpes92hAh2w==}
+
+ '@vue/compiler-ssr@3.6.0-rc.2':
+ resolution: {integrity: sha512-lqHSdQTmlhKX3dNEvR1TcspUzDlTR82T7BYLDzfvTwAqGXxAI2WJfyGJPcNpPjPKYEMI9JRGF32DU8TlHLx1AA==}
+
+ '@vue/compiler-vapor@3.6.0-rc.2':
+ resolution: {integrity: sha512-QbrA01pbj9xemF2McgBopyaOwj3kQGN0W1GW5EdFXORIT4fQQJpmUOj40h99S3bk09gLs5bFTRS4YE+Oyw/F9w==}
+
+ '@vue/devtools-api@8.2.1':
+ resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==}
+
+ '@vue/devtools-kit@8.2.1':
+ resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==}
+
+ '@vue/devtools-shared@8.2.1':
+ resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==}
+
+ '@vue/reactivity@3.6.0-rc.2':
+ resolution: {integrity: sha512-B01EoSTcaVQiJwZDoSoeKYClVgj60BHN43oHEcxXhPVLdxsXI0SOiQuskUskQDZn2ma08llICKJohL60o+UamA==}
+
+ '@vue/runtime-core@3.6.0-rc.2':
+ resolution: {integrity: sha512-qFV61Hbg6LyEGxJiVRJYuiIfryxrfO4+cgW1esxtkR1/pvemULwG6KpFImsJDwcC+YxrtvndXGM5PRVwUCGuiA==}
+
+ '@vue/runtime-dom@3.6.0-rc.2':
+ resolution: {integrity: sha512-clIE/xk0b61t30BPKoOXZep253nhWdjyw7iac53yy2KN0nxAupkb6x3ImOI9iljjXqOW5lYdNVHNxNlcx3iA1Q==}
+
+ '@vue/runtime-vapor@3.6.0-rc.2':
+ resolution: {integrity: sha512-8EpxbNyT629k/52el4D7PxXmZtiYhJRBHPXJR7CZw0OluiADtT9W8J+XB83EVlzHmFUysXcd+mGWmuHwIhuHZA==}
+ peerDependencies:
+ '@vue/runtime-dom': 3.6.0-rc.2
+
+ '@vue/server-renderer@3.6.0-rc.2':
+ resolution: {integrity: sha512-TIsqoYljs6b0wfoCDhv4EeU3Du2a8gTadkaTGMjIYLTLlO4p8r4VdDqfNp/ZMAm87bW4ST1naBMRe+bXQBpAeQ==}
+
+ '@vue/shared@3.6.0-rc.1':
+ resolution: {integrity: sha512-G2Y0ePuu7QIZ7eX7I8r0Q6mwO6rwhMXyYY8UAlPvkXsu7IxT5i8wDxiZK8EI5FnYYJKdSWInDnZREDX9w9zdCg==}
+
+ '@vue/shared@3.6.0-rc.2':
+ resolution: {integrity: sha512-Tz44lKLjxhWRu3GJ38768dc2hW4XhDtTB5aUo5fCMis6YDyFzwQYkrD/B9SuQ1v/ailv6UVR3wSHiujNXSjMCQ==}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.18.0:
+ resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.15.0:
+ resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ baseline-browser-mapping@2.11.12:
+ resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ birpc@2.9.0:
+ resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
+
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+ brace-expansion@5.0.9:
+ resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
+ engines: {node: 20 || >=22}
+
+ browserslist@4.28.7:
+ resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ builtin-modules@5.3.0:
+ resolution: {integrity: sha512-hMQUl2bUFG339QygPM97E+mc8OY1IAchORZxm4a/frcYwKzozMzRVDBwHW0NjOqGElLm2O37AVQE8ikxlZHrMQ==}
+ engines: {node: '>=18.20'}
+
+ caniuse-lite@1.0.30001806:
+ resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
+
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
+ change-case@5.4.4:
+ resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
+
+ ci-info@4.4.0:
+ resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
+ engines: {node: '>=8'}
+
+ comment-parser@1.4.7:
+ resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==}
+ engines: {node: '>= 12.0.0'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ core-js-compat@3.50.0:
+ resolution: {integrity: sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==}
+ engines: {node: '>=6.4.0'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ date-fns@4.4.0:
+ resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ detect-indent@7.0.2:
+ resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==}
+ engines: {node: '>=12.20'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ electron-to-chromium@1.5.401:
+ resolution: {integrity: sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==}
+
+ enhanced-resolve@5.24.5:
+ resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==}
+ engines: {node: '>=10.13.0'}
+
+ entities@7.0.1:
+ resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+ engines: {node: '>=0.12'}
+
+ es-module-lexer@2.3.1:
+ resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-compat-utils@0.5.1:
+ resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ eslint: '>=6.0.0'
+
+ eslint-import-context@0.1.9:
+ resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ unrs-resolver: ^1.0.0
+ peerDependenciesMeta:
+ unrs-resolver:
+ optional: true
+
+ eslint-plugin-es-x@7.8.0:
+ resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ eslint: '>=8'
+
+ eslint-plugin-import-x@4.17.1:
+ resolution: {integrity: sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/utils': ^8.56.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ eslint-import-resolver-node: '*'
+ peerDependenciesMeta:
+ '@typescript-eslint/utils':
+ optional: true
+ eslint-import-resolver-node:
+ optional: true
+
+ eslint-plugin-n@18.2.2:
+ resolution: {integrity: sha512-gOO0lIqwEjZ750kv9/SptCWArUoAZXJoBr0vYWTO2dCBxctHUXlBIigiC8xuxxr/NKqgIT6Ehz1xRcilj8a5cA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ peerDependencies:
+ eslint: '>=8.57.1'
+ ts-declaration-location: ^1.0.6
+ typescript: '>=5.0.0'
+ peerDependenciesMeta:
+ ts-declaration-location:
+ optional: true
+ typescript:
+ optional: true
+
+ eslint-plugin-regexp@3.1.1:
+ resolution: {integrity: sha512-MxR5nqoQCtVWmJwia0D2+NlXX1xzdpkslsVOZLEYQ4PQWEaL65PCZXURxaBc3lPnkNFpNxzMIRmYVxdl8giXRA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ peerDependencies:
+ eslint: '>=9.38.0'
+
+ eslint-plugin-unicorn@67.0.0:
+ resolution: {integrity: sha512-XEYhNfAFFFYQLq++53iqpgIPFqvjasf5lsUJuCdmd/QvbRfcLbmPTjcTyVEXc5yBudWIg08SSEGgqJtEojk1ug==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ eslint: '>=10.4'
+
+ eslint-plugin-vue@10.10.0:
+ resolution: {integrity: sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
+ '@typescript-eslint/parser': ^7.0.0 || ^8.0.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ vue-eslint-parser: ^10.3.0
+ peerDependenciesMeta:
+ '@stylistic/eslint-plugin':
+ optional: true
+ '@typescript-eslint/parser':
+ optional: true
+
+ eslint-scope@9.1.2:
+ resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.1:
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint@10.8.0:
+ resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ espree@11.2.0:
+ resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+ engines: {node: '>=12.0.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ find-up-simple@1.0.1:
+ resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
+ engines: {node: '>=18'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.4.4:
+ resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ get-tsconfig@4.14.1:
+ resolution: {integrity: sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@15.15.0:
+ resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
+ engines: {node: '>=18'}
+
+ globals@17.9.0:
+ resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==}
+ engines: {node: '>=18'}
+
+ globrex@0.1.2:
+ resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ hash-sum@2.0.0:
+ resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==}
+
+ hookable@5.5.3:
+ resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.6:
+ resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
+ engines: {node: '>= 4'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ indent-string@5.0.0:
+ resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
+ engines: {node: '>=12'}
+
+ is-builtin-module@5.0.0:
+ resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==}
+ engines: {node: '>=18.20'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
+ hasBin: true
+
+ jsdoc-type-pratt-parser@7.3.0:
+ resolution: {integrity: sha512-DoyJXo7x/n48M3NsGOs9QnEws0ft0tV3YsSgvWMNxz2hZtz+Q6fpqUD96lVYX4a+jcEkzHeFNDJOn68TzXfbdA==}
+ engines: {node: '>=20.0.0'}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-android-arm64@1.33.0:
+ resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-arm64@1.33.0:
+ resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.33.0:
+ resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-freebsd-x64@1.33.0:
+ resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-musl@1.33.0:
+ resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ lightningcss@1.33.0:
+ resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lucide@1.28.0:
+ resolution: {integrity: sha512-sxVxWEXfq7rRNzmffDBRohYBOFjfoOhrO7jGGCLNNRoY2fPjeuqi2boXR2UAROEyQUm+xe13DGOQNtnmwv6Y3w==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ minimatch@10.2.6:
+ resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
+ engines: {node: 18 || 20 || >=22}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ muggle-string@0.4.1:
+ resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
+
+ nanoid@3.3.17:
+ resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ napi-postinstall@0.3.4:
+ resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ node-releases@2.0.52:
+ resolution: {integrity: sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==}
+ engines: {node: '>=18'}
+
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ obug@2.1.4:
+ resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
+ engines: {node: '>=12.20.0'}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ perfect-debounce@2.1.0:
+ resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.5:
+ resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
+ engines: {node: '>=12'}
+
+ pluralize@8.0.0:
+ resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
+ engines: {node: '>=4'}
+
+ postcss-selector-parser@7.1.4:
+ resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==}
+ engines: {node: '>=4'}
+
+ postcss@8.5.25:
+ resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ refa@0.12.1:
+ resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ regexp-ast-analysis@0.7.1:
+ resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ regjsparser@0.13.2:
+ resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==}
+ hasBin: true
+
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ rolldown@1.2.3:
+ resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ scslre@0.3.0:
+ resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==}
+ engines: {node: ^14.0.0 || >=16.0.0}
+
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ stable-hash-x@0.2.0:
+ resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
+ engines: {node: '>=12.0.0'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@4.2.0:
+ resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
+
+ strip-indent@4.1.1:
+ resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==}
+ engines: {node: '>=12'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ tailwindcss@4.3.3:
+ resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==}
+
+ tapable@2.3.3:
+ resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
+ engines: {node: '>=6'}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.3.0:
+ resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.1.1:
+ resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
+ engines: {node: '>=14.0.0'}
+
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ ts-macro@0.3.7:
+ resolution: {integrity: sha512-5BinbTKn5WXRslotv5/VWTn3catfK0thWoB4BZI5VNEdxMu6r9AUPFalut4wLEK3nGvxhywA9aAdZnJ+Fnug9A==}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.66.0:
+ resolution: {integrity: sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.24.6:
+ resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
+
+ unplugin-utils@0.3.2:
+ resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==}
+ engines: {node: '>=20.19.0'}
+
+ unplugin@3.3.0:
+ resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@farmfe/core': '*'
+ '@rspack/core': '*'
+ bun-types-no-globals: '*'
+ esbuild: '*'
+ rolldown: '*'
+ rollup: '*'
+ unloader: '*'
+ vite: '*'
+ webpack: '*'
+ peerDependenciesMeta:
+ '@farmfe/core':
+ optional: true
+ '@rspack/core':
+ optional: true
+ bun-types-no-globals:
+ optional: true
+ esbuild:
+ optional: true
+ rolldown:
+ optional: true
+ rollup:
+ optional: true
+ unloader:
+ optional: true
+ vite:
+ optional: true
+ webpack:
+ optional: true
+
+ unrs-resolver@1.12.2:
+ resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ vite@8.2.1:
+ resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.4.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@4.1.10:
+ resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.1.10
+ '@vitest/browser-preview': 4.1.10
+ '@vitest/browser-webdriverio': 4.1.10
+ '@vitest/coverage-istanbul': 4.1.10
+ '@vitest/coverage-v8': 4.1.10
+ '@vitest/ui': 4.1.10
+ happy-dom: '*'
+ jsdom: '*'
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ vue-eslint-parser@10.4.1:
+ resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+
+ vue-jsx-vapor@3.2.19:
+ resolution: {integrity: sha512-jigvOolTWt7LrQzBp+JDSYuv64yH3u4Ymo8Vw4QBLXtzcv8Jua7HU37JMfi24lN1qC0n7WgyBPZox0RzNKWqJA==}
+ peerDependencies:
+ '@nuxt/kit': ^3
+ '@nuxt/schema': ^3
+ esbuild: '*'
+ rollup: ^3
+ vite: '>=3'
+ vue: ^3
+ webpack: ^4 || ^5
+ peerDependenciesMeta:
+ '@nuxt/kit':
+ optional: true
+ '@nuxt/schema':
+ optional: true
+ esbuild:
+ optional: true
+ rollup:
+ optional: true
+ vite:
+ optional: true
+ webpack:
+ optional: true
+
+ vue-sync-engine@file:../vue-sync-engine/lib:
+ resolution: {directory: ../vue-sync-engine/lib, type: directory}
+ peerDependencies:
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ vue: ^3.5.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ vue@3.6.0-rc.2:
+ resolution: {integrity: sha512-pBgoISqETcufJ1e92vVW2/D5MEbCm2XHlC/5dq3hWq8s17tx9v5BEdYv4hojJ0yEEDidYxUL7itS+vV32wfZrw==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ webpack-virtual-modules@0.6.2:
+ resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+snapshots:
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/parser@7.29.8':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@emnapi/core@1.10.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.1
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.10.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))':
+ dependencies:
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint/config-array@0.23.5(supports-color@7.2.0)':
+ dependencies:
+ '@eslint/object-schema': 3.0.5
+ debug: 4.4.3(supports-color@7.2.0)
+ minimatch: 10.2.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.7.0':
+ dependencies:
+ '@eslint/core': 1.2.1
+
+ '@eslint/core@1.2.1':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))':
+ optionalDependencies:
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+
+ '@eslint/object-schema@3.0.5': {}
+
+ '@eslint/plugin-kit@0.7.2':
+ dependencies:
+ '@eslint/core': 1.2.1
+ levn: 0.4.1
+
+ '@fontsource-variable/golos-text@5.3.0': {}
+
+ '@fontsource/spectral@5.3.0': {}
+
+ '@humanfs/core@0.19.2':
+ dependencies:
+ '@humanfs/types': 0.15.0
+
+ '@humanfs/node@0.16.8':
+ dependencies:
+ '@humanfs/core': 0.19.2
+ '@humanfs/types': 0.15.0
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanfs/types@0.15.0': {}
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@tybys/wasm-util': 0.10.3
+ optional: true
+
+ '@oxc-project/types@0.143.0': {}
+
+ '@robonen/eslint@0.0.1(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)(vitest@4.1.10(@types/node@25.9.5)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0)))':
+ dependencies:
+ '@eslint/js': 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ '@vitest/eslint-plugin': 1.6.26(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)(vitest@4.1.10(@types/node@25.9.5)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0)))
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+ eslint-plugin-n: 18.2.2(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@6.0.3)
+ eslint-plugin-regexp: 3.1.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ eslint-plugin-unicorn: 67.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ eslint-plugin-vue: 10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0))
+ globals: 17.9.0
+ jiti: 2.7.0
+ typescript-eslint: 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+ transitivePeerDependencies:
+ - '@typescript-eslint/eslint-plugin'
+ - '@typescript-eslint/parser'
+ - '@typescript-eslint/utils'
+ - eslint-import-resolver-node
+ - supports-color
+ - ts-declaration-location
+ - typescript
+ - vitest
+
+ '@robonen/platform@0.0.5': {}
+
+ '@robonen/stdlib@0.0.12': {}
+
+ '@robonen/tsconfig@0.1.0': {}
+
+ '@robonen/vue@0.2.0(vue@3.6.0-rc.2(typescript@6.0.3))':
+ dependencies:
+ '@robonen/platform': 0.0.5
+ '@robonen/stdlib': 0.0.12
+ vue: 3.6.0-rc.2(typescript@6.0.3)
+
+ '@rolldown/binding-android-arm64@1.2.3':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.2.3':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.2.3':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.2.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.2.3':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.3':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.2.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.2.3':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.2.3':
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.3':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.2.3':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ '@typescript-eslint/types': 8.66.0
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ estraverse: 5.3.0
+ picomatch: 4.0.5
+
+ '@tailwindcss/node@4.3.3':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.24.5
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.3.3
+
+ '@tailwindcss/oxide-android-arm64@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide@4.3.3':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.3.3
+ '@tailwindcss/oxide-darwin-arm64': 4.3.3
+ '@tailwindcss/oxide-darwin-x64': 4.3.3
+ '@tailwindcss/oxide-freebsd-x64': 4.3.3
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3
+ '@tailwindcss/oxide-linux-arm64-musl': 4.3.3
+ '@tailwindcss/oxide-linux-x64-gnu': 4.3.3
+ '@tailwindcss/oxide-linux-x64-musl': 4.3.3
+ '@tailwindcss/oxide-wasm32-wasi': 4.3.3
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3
+ '@tailwindcss/oxide-win32-x64-msvc': 4.3.3
+
+ '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))':
+ dependencies:
+ '@tailwindcss/node': 4.3.3
+ '@tailwindcss/oxide': 4.3.3
+ tailwindcss: 4.3.3
+ vite: 8.2.1(@types/node@25.9.5)(jiti@2.7.0)
+
+ '@tybys/wasm-util@0.10.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/esrecurse@4.3.1': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/node@25.9.5':
+ dependencies:
+ undici-types: 7.24.6
+
+ '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.66.0
+ '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.66.0
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ ignore: 7.0.6
+ natural-compare: 1.4.0
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.66.0
+ '@typescript-eslint/types': 8.66.0
+ '@typescript-eslint/typescript-estree': 8.66.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.66.0
+ debug: 4.4.3(supports-color@7.2.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.66.0(supports-color@7.2.0)(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.66.0
+ debug: 4.4.3(supports-color@7.2.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.66.0':
+ dependencies:
+ '@typescript-eslint/types': 8.66.0
+ '@typescript-eslint/visitor-keys': 8.66.0
+
+ '@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)':
+ dependencies:
+ typescript: 6.0.3
+
+ '@typescript-eslint/type-utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.66.0
+ '@typescript-eslint/typescript-estree': 8.66.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ debug: 4.4.3(supports-color@7.2.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.66.0': {}
+
+ '@typescript-eslint/typescript-estree@8.66.0(supports-color@7.2.0)(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.66.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.66.0
+ '@typescript-eslint/visitor-keys': 8.66.0
+ debug: 4.4.3(supports-color@7.2.0)
+ minimatch: 10.2.6
+ semver: 7.8.5
+ tinyglobby: 0.2.17
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ '@typescript-eslint/scope-manager': 8.66.0
+ '@typescript-eslint/types': 8.66.0
+ '@typescript-eslint/typescript-estree': 8.66.0(supports-color@7.2.0)(typescript@6.0.3)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.66.0':
+ dependencies:
+ '@typescript-eslint/types': 8.66.0
+ eslint-visitor-keys: 5.0.1
+
+ '@unrs/resolver-binding-android-arm-eabi@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-android-arm64@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-darwin-arm64@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-darwin-x64@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-freebsd-x64@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm64-gnu@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm64-musl@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-loong64-gnu@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-loong64-musl@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-riscv64-musl@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-s390x-gnu@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-x64-gnu@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-linux-x64-musl@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-openharmony-arm64@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-wasm32-wasi@1.12.2':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@unrs/resolver-binding-win32-arm64-msvc@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-win32-ia32-msvc@1.12.2':
+ optional: true
+
+ '@unrs/resolver-binding-win32-x64-msvc@1.12.2':
+ optional: true
+
+ '@vitest/eslint-plugin@1.6.26(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)(vitest@4.1.10(@types/node@25.9.5)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0)))':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.66.0
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ optionalDependencies:
+ '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ typescript: 6.0.3
+ vitest: 4.1.10(@types/node@25.9.5)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitest/expect@4.1.10':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.1.10
+ '@vitest/utils': 4.1.10
+ chai: 6.2.2
+ tinyrainbow: 3.1.1
+
+ '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))':
+ dependencies:
+ '@vitest/spy': 4.1.10
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 8.2.1(@types/node@25.9.5)(jiti@2.7.0)
+
+ '@vitest/pretty-format@4.1.10':
+ dependencies:
+ tinyrainbow: 3.1.1
+
+ '@vitest/runner@4.1.10':
+ dependencies:
+ '@vitest/utils': 4.1.10
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.1.10':
+ dependencies:
+ '@vitest/pretty-format': 4.1.10
+ '@vitest/utils': 4.1.10
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.1.10': {}
+
+ '@vitest/utils@4.1.10':
+ dependencies:
+ '@vitest/pretty-format': 4.1.10
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.1.1
+
+ '@vue-jsx-vapor/compiler-rs-android-arm-eabi@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-android-arm64@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-darwin-arm64@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-darwin-x64@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-freebsd-x64@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-linux-arm-gnueabihf@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-linux-arm64-gnu@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-linux-arm64-musl@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-linux-x64-gnu@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-linux-x64-musl@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-wasm32-wasi@3.2.19':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-win32-arm64-msvc@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-win32-ia32-msvc@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs-win32-x64-msvc@3.2.19':
+ optional: true
+
+ '@vue-jsx-vapor/compiler-rs@3.2.19':
+ optionalDependencies:
+ '@vue-jsx-vapor/compiler-rs-android-arm-eabi': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-android-arm64': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-darwin-arm64': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-darwin-x64': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-freebsd-x64': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-linux-arm-gnueabihf': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-linux-arm64-gnu': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-linux-arm64-musl': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-linux-x64-gnu': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-linux-x64-musl': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-wasm32-wasi': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-win32-arm64-msvc': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-win32-ia32-msvc': 3.2.19
+ '@vue-jsx-vapor/compiler-rs-win32-x64-msvc': 3.2.19
+
+ '@vue-jsx-vapor/macros@3.2.19(rolldown@1.2.3)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))(vue@3.6.0-rc.2(typescript@6.0.3))':
+ dependencies:
+ hash-sum: 2.0.0
+ magic-string: 0.30.21
+ ts-macro: 0.3.7
+ unplugin: 3.3.0(rolldown@1.2.3)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))
+ unplugin-utils: 0.3.2
+ vue: 3.6.0-rc.2(typescript@6.0.3)
+ optionalDependencies:
+ vite: 8.2.1(@types/node@25.9.5)(jiti@2.7.0)
+ transitivePeerDependencies:
+ - '@farmfe/core'
+ - '@rspack/core'
+ - bun-types-no-globals
+ - rolldown
+ - unloader
+
+ '@vue-jsx-vapor/runtime@3.2.19(csstype@3.2.3)(vue@3.6.0-rc.2(typescript@6.0.3))':
+ dependencies:
+ csstype: 3.2.3
+ vue: 3.6.0-rc.2(typescript@6.0.3)
+
+ '@vue/compiler-core@3.6.0-rc.2':
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@vue/shared': 3.6.0-rc.2
+ entities: 7.0.1
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-dom@3.6.0-rc.2':
+ dependencies:
+ '@vue/compiler-core': 3.6.0-rc.2
+ '@vue/shared': 3.6.0-rc.2
+
+ '@vue/compiler-sfc@3.6.0-rc.2':
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@vue/compiler-core': 3.6.0-rc.2
+ '@vue/compiler-dom': 3.6.0-rc.2
+ '@vue/compiler-ssr': 3.6.0-rc.2
+ '@vue/compiler-vapor': 3.6.0-rc.2
+ '@vue/shared': 3.6.0-rc.2
+ estree-walker: 2.0.2
+ magic-string: 0.30.21
+ postcss: 8.5.25
+ source-map-js: 1.2.1
+
+ '@vue/compiler-ssr@3.6.0-rc.2':
+ dependencies:
+ '@vue/compiler-dom': 3.6.0-rc.2
+ '@vue/shared': 3.6.0-rc.2
+
+ '@vue/compiler-vapor@3.6.0-rc.2':
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@vue/compiler-dom': 3.6.0-rc.2
+ '@vue/shared': 3.6.0-rc.2
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/devtools-api@8.2.1':
+ dependencies:
+ '@vue/devtools-kit': 8.2.1
+
+ '@vue/devtools-kit@8.2.1':
+ dependencies:
+ '@vue/devtools-shared': 8.2.1
+ birpc: 2.9.0
+ hookable: 5.5.3
+ perfect-debounce: 2.1.0
+
+ '@vue/devtools-shared@8.2.1': {}
+
+ '@vue/reactivity@3.6.0-rc.2':
+ dependencies:
+ '@vue/shared': 3.6.0-rc.2
+
+ '@vue/runtime-core@3.6.0-rc.2':
+ dependencies:
+ '@vue/reactivity': 3.6.0-rc.2
+ '@vue/shared': 3.6.0-rc.2
+
+ '@vue/runtime-dom@3.6.0-rc.2':
+ dependencies:
+ '@vue/reactivity': 3.6.0-rc.2
+ '@vue/runtime-core': 3.6.0-rc.2
+ '@vue/shared': 3.6.0-rc.2
+ csstype: 3.2.3
+
+ '@vue/runtime-vapor@3.6.0-rc.2(@vue/runtime-dom@3.6.0-rc.2)':
+ dependencies:
+ '@vue/reactivity': 3.6.0-rc.2
+ '@vue/runtime-dom': 3.6.0-rc.2
+ '@vue/shared': 3.6.0-rc.2
+
+ '@vue/server-renderer@3.6.0-rc.2':
+ dependencies:
+ '@vue/compiler-ssr': 3.6.0-rc.2
+ '@vue/runtime-dom': 3.6.0-rc.2
+ '@vue/shared': 3.6.0-rc.2
+
+ '@vue/shared@3.6.0-rc.1': {}
+
+ '@vue/shared@3.6.0-rc.2': {}
+
+ acorn-jsx@5.3.2(acorn@8.18.0):
+ dependencies:
+ acorn: 8.18.0
+
+ acorn@8.18.0: {}
+
+ ajv@6.15.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ assertion-error@2.0.1: {}
+
+ balanced-match@4.0.4: {}
+
+ baseline-browser-mapping@2.11.12: {}
+
+ birpc@2.9.0: {}
+
+ boolbase@1.0.0: {}
+
+ brace-expansion@5.0.9:
+ dependencies:
+ balanced-match: 4.0.4
+
+ browserslist@4.28.7:
+ dependencies:
+ baseline-browser-mapping: 2.11.12
+ caniuse-lite: 1.0.30001806
+ electron-to-chromium: 1.5.401
+ node-releases: 2.0.52
+ update-browserslist-db: 1.2.3(browserslist@4.28.7)
+
+ builtin-modules@5.3.0: {}
+
+ caniuse-lite@1.0.30001806: {}
+
+ chai@6.2.2: {}
+
+ change-case@5.4.4: {}
+
+ ci-info@4.4.0: {}
+
+ comment-parser@1.4.7: {}
+
+ convert-source-map@2.0.0: {}
+
+ core-js-compat@3.50.0:
+ dependencies:
+ browserslist: 4.28.7
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ cssesc@3.0.0: {}
+
+ csstype@3.2.3: {}
+
+ date-fns@4.4.0: {}
+
+ debug@4.4.3(supports-color@7.2.0):
+ dependencies:
+ ms: 2.1.3
+ optionalDependencies:
+ supports-color: 7.2.0
+
+ deep-is@0.1.4: {}
+
+ detect-indent@7.0.2: {}
+
+ detect-libc@2.1.2: {}
+
+ electron-to-chromium@1.5.401: {}
+
+ enhanced-resolve@5.24.5:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.3
+
+ entities@7.0.1: {}
+
+ es-module-lexer@2.3.1: {}
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-compat-utils@0.5.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)):
+ dependencies:
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ semver: 7.8.5
+
+ eslint-import-context@0.1.9(unrs-resolver@1.12.2):
+ dependencies:
+ get-tsconfig: 4.14.1
+ stable-hash-x: 0.2.0
+ optionalDependencies:
+ unrs-resolver: 1.12.2
+
+ eslint-plugin-es-x@7.8.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ '@eslint-community/regexpp': 4.12.2
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-compat-utils: 0.5.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+
+ eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
+ dependencies:
+ '@typescript-eslint/types': 8.66.0
+ comment-parser: 1.4.7
+ debug: 4.4.3(supports-color@7.2.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-import-context: 0.1.9(unrs-resolver@1.12.2)
+ is-glob: 4.0.3
+ minimatch: 10.2.6
+ semver: 7.8.5
+ stable-hash-x: 0.2.0
+ unrs-resolver: 1.12.2
+ optionalDependencies:
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-n@18.2.2(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@6.0.3):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ enhanced-resolve: 5.24.5
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-plugin-es-x: 7.8.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ get-tsconfig: 4.14.1
+ globals: 15.15.0
+ globrex: 0.1.2
+ ignore: 5.3.2
+ semver: 7.8.5
+ optionalDependencies:
+ typescript: 6.0.3
+
+ eslint-plugin-regexp@3.1.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ '@eslint-community/regexpp': 4.12.2
+ comment-parser: 1.4.7
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ jsdoc-type-pratt-parser: 7.3.0
+ refa: 0.12.1
+ regexp-ast-analysis: 0.7.1
+ scslre: 0.3.0
+
+ eslint-plugin-unicorn@67.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)):
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ browserslist: 4.28.7
+ change-case: 5.4.4
+ ci-info: 4.4.0
+ core-js-compat: 3.50.0
+ detect-indent: 7.0.2
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ find-up-simple: 1.0.1
+ globals: 17.9.0
+ indent-string: 5.0.0
+ is-builtin-module: 5.0.0
+ jsesc: 3.1.0
+ pluralize: 8.0.0
+ regjsparser: 0.13.2
+ semver: 7.8.5
+ strip-indent: 4.1.1
+
+ eslint-plugin-vue@10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ natural-compare: 1.4.0
+ nth-check: 2.1.1
+ postcss-selector-parser: 7.1.4
+ semver: 7.8.5
+ vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+ xml-name-validator: 5.0.0
+ optionalDependencies:
+ '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+
+ eslint-scope@9.1.2:
+ dependencies:
+ '@types/esrecurse': 4.3.1
+ '@types/estree': 1.0.9
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint-visitor-keys@5.0.1: {}
+
+ eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.23.5(supports-color@7.2.0)
+ '@eslint/config-helpers': 0.7.0
+ '@eslint/core': 1.2.1
+ '@eslint/plugin-kit': 0.7.2
+ '@humanfs/node': 0.16.8
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.9
+ ajv: 6.15.0
+ cross-spawn: 7.0.6
+ debug: 4.4.3(supports-color@7.2.0)
+ escape-string-regexp: 4.0.0
+ eslint-scope: 9.1.2
+ eslint-visitor-keys: 5.0.1
+ espree: 11.2.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ minimatch: 10.2.6
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.7.0
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.18.0
+ acorn-jsx: 5.3.2(acorn@8.18.0)
+ eslint-visitor-keys: 4.2.1
+
+ espree@11.2.0:
+ dependencies:
+ acorn: 8.18.0
+ acorn-jsx: 5.3.2(acorn@8.18.0)
+ eslint-visitor-keys: 5.0.1
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-walker@2.0.2: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ esutils@2.0.3: {}
+
+ expect-type@1.4.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fdir@6.5.0(picomatch@4.0.5):
+ optionalDependencies:
+ picomatch: 4.0.5
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ find-up-simple@1.0.1: {}
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.4.4
+ keyv: 4.5.4
+
+ flatted@3.4.4: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ get-tsconfig@4.14.1:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@15.15.0: {}
+
+ globals@17.9.0: {}
+
+ globrex@0.1.2: {}
+
+ graceful-fs@4.2.11: {}
+
+ has-flag@4.0.0:
+ optional: true
+
+ hash-sum@2.0.0: {}
+
+ hookable@5.5.3: {}
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.6: {}
+
+ imurmurhash@0.1.4: {}
+
+ indent-string@5.0.0: {}
+
+ is-builtin-module@5.0.0:
+ dependencies:
+ builtin-modules: 5.3.0
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ isexe@2.0.0: {}
+
+ jiti@2.7.0: {}
+
+ jsdoc-type-pratt-parser@7.3.0: {}
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-android-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.33.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.33.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ lightningcss@1.33.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.33.0
+ lightningcss-darwin-arm64: 1.33.0
+ lightningcss-darwin-x64: 1.33.0
+ lightningcss-freebsd-x64: 1.33.0
+ lightningcss-linux-arm-gnueabihf: 1.33.0
+ lightningcss-linux-arm64-gnu: 1.33.0
+ lightningcss-linux-arm64-musl: 1.33.0
+ lightningcss-linux-x64-gnu: 1.33.0
+ lightningcss-linux-x64-musl: 1.33.0
+ lightningcss-win32-arm64-msvc: 1.33.0
+ lightningcss-win32-x64-msvc: 1.33.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lucide@1.28.0: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ minimatch@10.2.6:
+ dependencies:
+ brace-expansion: 5.0.9
+
+ ms@2.1.3: {}
+
+ muggle-string@0.4.1: {}
+
+ nanoid@3.3.17: {}
+
+ napi-postinstall@0.3.4: {}
+
+ natural-compare@1.4.0: {}
+
+ node-releases@2.0.52: {}
+
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
+ obug@2.1.4: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ pathe@2.0.3: {}
+
+ perfect-debounce@2.1.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.5: {}
+
+ pluralize@8.0.0: {}
+
+ postcss-selector-parser@7.1.4:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss@8.5.25:
+ dependencies:
+ nanoid: 3.3.17
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ punycode@2.3.1: {}
+
+ refa@0.12.1:
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+
+ regexp-ast-analysis@0.7.1:
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ refa: 0.12.1
+
+ regjsparser@0.13.2:
+ dependencies:
+ jsesc: 3.1.0
+
+ resolve-pkg-maps@1.0.0: {}
+
+ rolldown@1.2.3:
+ dependencies:
+ '@oxc-project/types': 0.143.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.2.3
+ '@rolldown/binding-darwin-arm64': 1.2.3
+ '@rolldown/binding-darwin-x64': 1.2.3
+ '@rolldown/binding-freebsd-x64': 1.2.3
+ '@rolldown/binding-linux-arm-gnueabihf': 1.2.3
+ '@rolldown/binding-linux-arm64-gnu': 1.2.3
+ '@rolldown/binding-linux-arm64-musl': 1.2.3
+ '@rolldown/binding-linux-ppc64-gnu': 1.2.3
+ '@rolldown/binding-linux-s390x-gnu': 1.2.3
+ '@rolldown/binding-linux-x64-gnu': 1.2.3
+ '@rolldown/binding-linux-x64-musl': 1.2.3
+ '@rolldown/binding-openharmony-arm64': 1.2.3
+ '@rolldown/binding-win32-arm64-msvc': 1.2.3
+ '@rolldown/binding-win32-x64-msvc': 1.2.3
+
+ scslre@0.3.0:
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ refa: 0.12.1
+ regexp-ast-analysis: 0.7.1
+
+ semver@7.8.5: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ siginfo@2.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ stable-hash-x@0.2.0: {}
+
+ stackback@0.0.2: {}
+
+ std-env@4.2.0: {}
+
+ strip-indent@4.1.1: {}
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+ optional: true
+
+ tailwindcss@4.3.3: {}
+
+ tapable@2.3.3: {}
+
+ tinybench@2.9.0: {}
+
+ tinyexec@1.3.0: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+
+ tinyrainbow@3.1.1: {}
+
+ ts-api-utils@2.5.0(typescript@6.0.3):
+ dependencies:
+ typescript: 6.0.3
+
+ ts-macro@0.3.7:
+ dependencies:
+ muggle-string: 0.4.1
+
+ tslib@2.8.1:
+ optional: true
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.66.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@6.0.3: {}
+
+ undici-types@7.24.6: {}
+
+ unplugin-utils@0.3.2:
+ dependencies:
+ pathe: 2.0.3
+ picomatch: 4.0.5
+
+ unplugin@3.3.0(rolldown@1.2.3)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0)):
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ picomatch: 4.0.5
+ webpack-virtual-modules: 0.6.2
+ optionalDependencies:
+ rolldown: 1.2.3
+ vite: 8.2.1(@types/node@25.9.5)(jiti@2.7.0)
+
+ unrs-resolver@1.12.2:
+ dependencies:
+ napi-postinstall: 0.3.4
+ optionalDependencies:
+ '@unrs/resolver-binding-android-arm-eabi': 1.12.2
+ '@unrs/resolver-binding-android-arm64': 1.12.2
+ '@unrs/resolver-binding-darwin-arm64': 1.12.2
+ '@unrs/resolver-binding-darwin-x64': 1.12.2
+ '@unrs/resolver-binding-freebsd-x64': 1.12.2
+ '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2
+ '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2
+ '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2
+ '@unrs/resolver-binding-linux-arm64-musl': 1.12.2
+ '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2
+ '@unrs/resolver-binding-linux-loong64-musl': 1.12.2
+ '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2
+ '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2
+ '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2
+ '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2
+ '@unrs/resolver-binding-linux-x64-gnu': 1.12.2
+ '@unrs/resolver-binding-linux-x64-musl': 1.12.2
+ '@unrs/resolver-binding-openharmony-arm64': 1.12.2
+ '@unrs/resolver-binding-wasm32-wasi': 1.12.2
+ '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2
+ '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2
+ '@unrs/resolver-binding-win32-x64-msvc': 1.12.2
+
+ update-browserslist-db@1.2.3(browserslist@4.28.7):
+ dependencies:
+ browserslist: 4.28.7
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ util-deprecate@1.0.2: {}
+
+ vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0):
+ dependencies:
+ lightningcss: 1.33.0
+ picomatch: 4.0.5
+ postcss: 8.5.25
+ rolldown: 1.2.3
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 25.9.5
+ fsevents: 2.3.3
+ jiti: 2.7.0
+
+ vitest@4.1.10(@types/node@25.9.5)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0)):
+ dependencies:
+ '@vitest/expect': 4.1.10
+ '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))
+ '@vitest/pretty-format': 4.1.10
+ '@vitest/runner': 4.1.10
+ '@vitest/snapshot': 4.1.10
+ '@vitest/spy': 4.1.10
+ '@vitest/utils': 4.1.10
+ es-module-lexer: 2.3.1
+ expect-type: 1.4.0
+ magic-string: 0.30.21
+ obug: 2.1.4
+ pathe: 2.0.3
+ picomatch: 4.0.5
+ std-env: 4.2.0
+ tinybench: 2.9.0
+ tinyexec: 1.3.0
+ tinyglobby: 0.2.17
+ tinyrainbow: 3.1.1
+ vite: 8.2.1(@types/node@25.9.5)(jiti@2.7.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 25.9.5
+ transitivePeerDependencies:
+ - msw
+
+ vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
+ dependencies:
+ debug: 4.4.3(supports-color@7.2.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-scope: 9.1.2
+ eslint-visitor-keys: 5.0.1
+ espree: 11.2.0
+ esquery: 1.7.0
+ semver: 7.8.5
+ transitivePeerDependencies:
+ - supports-color
+
+ vue-jsx-vapor@3.2.19(csstype@3.2.3)(rolldown@1.2.3)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))(vue@3.6.0-rc.2(typescript@6.0.3)):
+ dependencies:
+ '@vue-jsx-vapor/compiler-rs': 3.2.19
+ '@vue-jsx-vapor/macros': 3.2.19(rolldown@1.2.3)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))(vue@3.6.0-rc.2(typescript@6.0.3))
+ '@vue-jsx-vapor/runtime': 3.2.19(csstype@3.2.3)(vue@3.6.0-rc.2(typescript@6.0.3))
+ '@vue/shared': 3.6.0-rc.1
+ pathe: 2.0.3
+ ts-macro: 0.3.7
+ unplugin: 3.3.0(rolldown@1.2.3)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))
+ unplugin-utils: 0.3.2
+ vue: 3.6.0-rc.2(typescript@6.0.3)
+ optionalDependencies:
+ vite: 8.2.1(@types/node@25.9.5)(jiti@2.7.0)
+ transitivePeerDependencies:
+ - '@farmfe/core'
+ - '@rspack/core'
+ - bun-types-no-globals
+ - csstype
+ - rolldown
+ - unloader
+
+ vue-sync-engine@file:../vue-sync-engine/lib(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))(vue@3.6.0-rc.2(typescript@6.0.3)):
+ dependencies:
+ '@vue/devtools-api': 8.2.1
+ vue: 3.6.0-rc.2(typescript@6.0.3)
+ optionalDependencies:
+ vite: 8.2.1(@types/node@25.9.5)(jiti@2.7.0)
+
+ vue@3.6.0-rc.2(typescript@6.0.3):
+ dependencies:
+ '@vue/compiler-dom': 3.6.0-rc.2
+ '@vue/compiler-sfc': 3.6.0-rc.2
+ '@vue/runtime-dom': 3.6.0-rc.2
+ '@vue/runtime-vapor': 3.6.0-rc.2(@vue/runtime-dom@3.6.0-rc.2)
+ '@vue/server-renderer': 3.6.0-rc.2
+ '@vue/shared': 3.6.0-rc.2
+ optionalDependencies:
+ typescript: 6.0.3
+
+ webpack-virtual-modules@0.6.2: {}
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ word-wrap@1.2.5: {}
+
+ xml-name-validator@5.0.0: {}
+
+ yocto-queue@0.1.0: {}
diff --git a/kcal/pnpm-workspace.yaml b/kcal/pnpm-workspace.yaml
new file mode 100644
index 0000000..ed33aa1
--- /dev/null
+++ b/kcal/pnpm-workspace.yaml
@@ -0,0 +1,8 @@
+allowBuilds:
+ unrs-resolver: true
+minimumReleaseAgeExclude:
+ - vite@8.2.1
+peerDependencyRules:
+ allowAny:
+ - vue
+ - rollup
diff --git a/kcal/src/App.tsx b/kcal/src/App.tsx
new file mode 100644
index 0000000..3a5ee4d
--- /dev/null
+++ b/kcal/src/App.tsx
@@ -0,0 +1,117 @@
+import { computed, watchEffect } from 'vue';
+import { useQuery } from 'vue-sync-engine';
+import { profileQuery } from './data/defs';
+import type { Meal } from './domain/types';
+import { activeTab, addSheet, editEntryId, foodForm, openAddSheet } from './ui/state';
+import type { Tab } from './ui/state';
+import { IconApple, IconBook, IconChart, IconPlus, IconUser } from './ui/icons';
+import DiaryScreen from './screens/DiaryScreen';
+import StatsScreen from './screens/StatsScreen';
+import FoodsScreen from './screens/FoodsScreen';
+import ProfileScreen from './screens/ProfileScreen';
+import AddSheet from './screens/AddSheet';
+import EditEntrySheet from './screens/EditEntrySheet';
+import FoodFormSheet from './screens/FoodFormSheet';
+
+const TABS: ReadonlyArray<{ id: Tab; label: string }> = [
+ { id: 'diary', label: 'Дневник' },
+ { id: 'stats', label: 'Статистика' },
+ { id: 'foods', label: 'Продукты' },
+ { id: 'profile', label: 'Профиль' },
+];
+
+/** Приём пищи по времени суток — разумный дефолт для кнопки «+». */
+function mealByHour(hour: number): Meal {
+ if (hour < 11) return 'breakfast';
+ if (hour < 16) return 'lunch';
+ if (hour < 21) return 'dinner';
+ return 'snack';
+}
+
+function tabIcon(tab: Tab, active: boolean) {
+ const cls = `size-5.5 ${active ? 'text-ember-bright' : 'text-ink-faint'}`;
+ if (tab === 'diary') return ;
+ if (tab === 'stats') return ;
+ if (tab === 'foods') return ;
+ return ;
+}
+
+export default function App() {
+ const profileQ = useQuery(profileQuery, () => undefined);
+ const ready = computed(() => profileQ.data.value !== undefined);
+ const hasProfile = computed(() => profileQ.data.value?.exists === true);
+
+ const sheetOpen = computed(() => addSheet.open || editEntryId.value !== null || foodForm.open);
+ watchEffect(() => {
+ document.documentElement.classList.toggle('overflow-hidden', sheetOpen.value);
+ });
+
+ return (
+
+ {!ready.value && (
+
+ Ккал
+
+ )}
+
+ {ready.value && !hasProfile.value && (
+
+ )}
+
+ {ready.value && hasProfile.value && (
+ <>
+
+ {activeTab.value === 'diary' && }
+ {activeTab.value === 'stats' && }
+ {activeTab.value === 'foods' && }
+ {activeTab.value === 'profile' && }
+
+
+ {/* Нижняя навигация */}
+
+
+ {TABS.slice(0, 2).map(tab => (
+
(activeTab.value = tab.id)}
+ >
+ {tabIcon(tab.id, activeTab.value === tab.id)}
+ {tab.label}
+
+ ))}
+
+
+ openAddSheet(mealByHour(new Date().getHours()))}
+ >
+
+
+
+
+ {TABS.slice(2).map(tab => (
+
(activeTab.value = tab.id)}
+ >
+ {tabIcon(tab.id, activeTab.value === tab.id)}
+ {tab.label}
+
+ ))}
+
+
+ >
+ )}
+
+ {addSheet.open &&
}
+ {editEntryId.value !== null &&
}
+ {foodForm.open &&
}
+
+ );
+}
diff --git a/kcal/src/app.css b/kcal/src/app.css
new file mode 100644
index 0000000..078d6eb
--- /dev/null
+++ b/kcal/src/app.css
@@ -0,0 +1,134 @@
+@import 'tailwindcss';
+
+/*
+ * «Вечерний гроссбух»: тёплый угольный фон, кремовые чернила, янтарный акцент.
+ * Цвета данных (ember/protein/fat/carbs) проверены на CVD-разделимость и
+ * контраст к поверхности (OKLCH L 0.48–0.67, C ≥ 0.1, ΔE пар ≥ 8).
+ */
+@theme {
+ --font-sans: 'Golos Text Variable', 'Golos Text', system-ui, sans-serif;
+ --font-display: 'Spectral', 'Iowan Old Style', Georgia, serif;
+
+ --color-bg: #12100d;
+ --color-surface: #1b1815;
+ --color-raised: #262119;
+ --color-ink: #ece4d6;
+ --color-ink-soft: #b3a894;
+ --color-ink-faint: #80766a;
+
+ --color-ember: #cf7728;
+ --color-ember-bright: #eda05b;
+ --color-protein: #2aa08b;
+ --color-fat: #ae8e2c;
+ --color-carbs: #6284d0;
+ --color-over: #cf5f4d;
+ --color-over-bright: #e8836f;
+}
+
+html {
+ background: var(--color-bg);
+ color-scheme: dark;
+}
+
+body {
+ font-family: var(--font-sans);
+ color: var(--color-ink);
+ background:
+ radial-gradient(120% 70% at 50% 0%, #201b14 0%, var(--color-bg) 55%),
+ var(--color-bg);
+ min-height: 100dvh;
+}
+
+/* Зерно поверх фона — атмосфера бумаги при свете лампы. */
+body::after {
+ content: '';
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 70;
+ opacity: 0.05;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
+}
+
+@utility text-display {
+ font-family: var(--font-display);
+ font-variant-numeric: lining-nums;
+ letter-spacing: -0.01em;
+}
+
+@utility tnum {
+ font-variant-numeric: tabular-nums lining-nums;
+}
+
+@utility hairline {
+ border-color: color-mix(in oklab, var(--color-ink) 10%, transparent);
+}
+
+@utility scrollbar-none {
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
+
+/* Появление контента: мягкий подъём, каскад через animation-delay. */
+@keyframes rise {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@utility animate-rise {
+ animation: rise 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
+}
+
+@keyframes sheet-up {
+ from {
+ transform: translateY(100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+@utility animate-sheet-up {
+ animation: sheet-up 0.38s cubic-bezier(0.22, 1, 0.36, 1) both;
+}
+
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@utility animate-fade-in {
+ animation: fade-in 0.25s ease-out both;
+}
+
+/* Кольцо калорий: плавное перетекание дуги при изменении данных. */
+.ring-arc {
+ transition: stroke-dashoffset 0.7s cubic-bezier(0.22, 1, 0.36, 1), stroke 0.4s ease;
+}
+
+.bar-fill {
+ transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1), background-color 0.3s ease;
+}
+
+/* Прячем спиннеры числовых инпутов — порции вводятся с клавиатуры и чипсами. */
+input[type='number']::-webkit-outer-spin-button,
+input[type='number']::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+input[type='number'] {
+ -moz-appearance: textfield;
+ appearance: textfield;
+}
diff --git a/kcal/src/components/BarcodeScanner.tsx b/kcal/src/components/BarcodeScanner.tsx
new file mode 100644
index 0000000..c039a7f
--- /dev/null
+++ b/kcal/src/components/BarcodeScanner.tsx
@@ -0,0 +1,94 @@
+import { onMounted, onUnmounted, shallowRef } from 'vue';
+import { useRef } from 'vue-jsx-vapor';
+
+/** Есть ли в этом браузере нативное распознавание штрихкодов (Chromium). */
+export function isBarcodeScanSupported(): boolean {
+ return typeof BarcodeDetector !== 'undefined' && !!navigator.mediaDevices?.getUserMedia;
+}
+
+const SCAN_INTERVAL_MS = 300;
+
+/**
+ * Камера + BarcodeDetector: распознали EAN/UPC — отдали наверх и погасили
+ * камеру. Компонент монтируется только после isBarcodeScanSupported().
+ */
+export default function BarcodeScanner(props: { onDetected: (code: string) => void; onCancel: () => void }) {
+ const videoEl = useRef();
+ const error = shallowRef('');
+
+ let stream: MediaStream | null = null;
+ let timer: ReturnType | null = null;
+ let finished = false;
+
+ const stop = () => {
+ if (timer !== null) clearInterval(timer);
+ timer = null;
+ stream?.getTracks().forEach(track => track.stop());
+ stream = null;
+ };
+
+ onMounted(async () => {
+ const video = videoEl.value as HTMLVideoElement | null;
+ if (!video) return;
+ try {
+ stream = await navigator.mediaDevices.getUserMedia({
+ video: { facingMode: 'environment' },
+ audio: false,
+ });
+ video.srcObject = stream;
+ await video.play();
+
+ const detector = new BarcodeDetector({
+ formats: ['ean_13', 'ean_8', 'upc_a', 'upc_e', 'code_128'],
+ });
+ timer = setInterval(async () => {
+ if (finished || video.readyState < 2) return;
+ try {
+ const barcodes = await detector.detect(video);
+ const code = barcodes[0]?.rawValue;
+ if (code) {
+ finished = true;
+ stop();
+ props.onDetected(code);
+ }
+ }
+ catch {
+ // Единичный сбой кадра — просто ждём следующий тик.
+ }
+ }, SCAN_INTERVAL_MS);
+ }
+ catch (cause) {
+ error.value = cause instanceof DOMException && cause.name === 'NotAllowedError'
+ ? 'Нет доступа к камере — разрешите его в настройках браузера.'
+ : 'Не удалось включить камеру.';
+ }
+ });
+
+ onUnmounted(stop);
+
+ return (
+
+ {error.value === ''
+ ? (
+
+
+ {/* Рамка прицела */}
+
+
+ Наведите на штрихкод упаковки
+
+
+ )
+ :
{error.value}
}
+
+ Отмена
+
+
+ );
+}
diff --git a/kcal/src/components/MacroBar.tsx b/kcal/src/components/MacroBar.tsx
new file mode 100644
index 0000000..ae61d86
--- /dev/null
+++ b/kcal/src/components/MacroBar.tsx
@@ -0,0 +1,37 @@
+import { computed } from 'vue';
+import { clamp } from '@robonen/stdlib';
+import { fmtG } from '../domain/format';
+
+const COLORS = {
+ protein: 'var(--color-protein)',
+ fat: 'var(--color-fat)',
+ carbs: 'var(--color-carbs)',
+} as const;
+
+/** Полоса «съедено/цель» по одному макронутриенту, в граммах. */
+export default function MacroBar(props: {
+ label: string;
+ value: number;
+ target: number;
+ color: keyof typeof COLORS;
+}) {
+ const percent = computed(() => (props.target > 0 ? clamp((props.value / props.target) * 100, 0, 100) : 0));
+
+ return (
+
+
+
+ {props.label}
+
+
+
+ {`${fmtG(props.value)} / ${fmtG(props.target)} г`}
+
+
+ );
+}
diff --git a/kcal/src/components/ProgressRing.tsx b/kcal/src/components/ProgressRing.tsx
new file mode 100644
index 0000000..30c768e
--- /dev/null
+++ b/kcal/src/components/ProgressRing.tsx
@@ -0,0 +1,64 @@
+import { computed } from 'vue';
+import { clamp } from '@robonen/stdlib';
+import { fmtKcal } from '../domain/format';
+
+const SIZE = 216;
+const RADIUS = 92;
+const STROKE = 11;
+const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
+
+/**
+ * Главный индикатор дня: съедено против цели, в центре — остаток.
+ * При переборе дуга замыкается и меняет цвет, остаток становится «сверх цели».
+ */
+export default function ProgressRing(props: { eaten: number; target: number }) {
+ const over = computed(() => props.eaten > props.target);
+ const fraction = computed(() => (props.target > 0 ? clamp(props.eaten / props.target, 0, 1) : 0));
+ const dashOffset = computed(() => CIRCUMFERENCE * (1 - fraction.value));
+ const remaining = computed(() => Math.abs(props.target - props.eaten));
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {fmtKcal(remaining.value)}
+
+
+ {over.value ? 'ккал сверх цели' : 'ккал осталось'}
+
+
+ {`${fmtKcal(props.eaten)} из ${fmtKcal(props.target)}`}
+
+
+
+ );
+}
diff --git a/kcal/src/data/backup.ts b/kcal/src/data/backup.ts
new file mode 100644
index 0000000..9dfe677
--- /dev/null
+++ b/kcal/src/data/backup.ts
@@ -0,0 +1,57 @@
+import { entryStore, foodStore, profileStore, weightStore } from './defs';
+import type { Entry, Food, Profile, WeightLog } from '../domain/types';
+
+export interface BackupPayload {
+ app: 'kcal';
+ version: 1;
+ exportedAt: string;
+ foods: Food[];
+ entries: Entry[];
+ weights: WeightLog[];
+ profile: Profile | null;
+}
+
+/** Полный снимок данных для файла-бэкапа. */
+export async function exportBackup(): Promise {
+ const [foods, entries, weights, profiles] = await Promise.all([
+ foodStore.readAll(),
+ entryStore.readAll(),
+ weightStore.readAll(),
+ profileStore.readAll(),
+ ]);
+ return {
+ app: 'kcal',
+ version: 1,
+ exportedAt: new Date().toISOString(),
+ foods,
+ entries,
+ weights,
+ profile: profiles[0] ?? null,
+ };
+}
+
+/**
+ * Восстановление из бэкапа: данные дописываются поверх текущих (merge по id).
+ * После импорта проще всего перезагрузить страницу — кэш запросов пересоберётся.
+ */
+export async function importBackup(payload: BackupPayload): Promise {
+ if (payload.app !== 'kcal' || payload.version !== 1) {
+ throw new Error('Файл не похож на бэкап приложения «Ккал»');
+ }
+ await Promise.all([
+ payload.foods.length > 0 ? foodStore.write(payload.foods.map(f => ({ key: f.id, value: f }))) : Promise.resolve(),
+ payload.entries.length > 0 ? entryStore.write(payload.entries.map(e => ({ key: e.id, value: e }))) : Promise.resolve(),
+ payload.weights.length > 0 ? weightStore.write(payload.weights.map(w => ({ key: w.id, value: w }))) : Promise.resolve(),
+ payload.profile ? profileStore.write([{ key: payload.profile.id, value: payload.profile }]) : Promise.resolve(),
+ ]);
+}
+
+export function downloadBackupFile(payload: BackupPayload): void {
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `kcal-backup-${payload.exportedAt.slice(0, 10)}.json`;
+ link.click();
+ URL.revokeObjectURL(url);
+}
diff --git a/kcal/src/data/composables.ts b/kcal/src/data/composables.ts
new file mode 100644
index 0000000..3f3d1b9
--- /dev/null
+++ b/kcal/src/data/composables.ts
@@ -0,0 +1,25 @@
+import { computed, toValue } from 'vue';
+import type { ComputedRef, MaybeRefOrGetter } from 'vue';
+import { useEngine } from 'vue-sync-engine';
+import type { EntityDef } from 'vue-sync-engine';
+
+/**
+ * Реактивный список сущностей по массиву id из нормализованного результата
+ * запроса. Mirror отдаёт per-entity версии, поэтому optimistic-патч одной
+ * записи не пересобирает соседей.
+ */
+export function useEntities(
+ def: EntityDef,
+ ids: MaybeRefOrGetter,
+): ComputedRef {
+ const engine = useEngine();
+ return computed(() => {
+ const list = toValue(ids) ?? [];
+ const items: T[] = [];
+ for (const id of list) {
+ const item = engine.mirror.getEntity(def.name, id);
+ if (item !== undefined) items.push(item);
+ }
+ return items;
+ });
+}
diff --git a/kcal/src/data/defs.ts b/kcal/src/data/defs.ts
new file mode 100644
index 0000000..2134801
--- /dev/null
+++ b/kcal/src/data/defs.ts
@@ -0,0 +1,232 @@
+import { defineEntity, defineMutation, defineQuery, idbStore } from 'vue-sync-engine';
+import type { Entry, Food, Profile, WeightLog } from '../domain/types';
+import { PROFILE_ID } from '../domain/types';
+
+/**
+ * Бэкенда нет: источник истины — IndexedDB. Каждая сущность персистится в свой
+ * object store базы `kcal`; query.fetch читает оттуда же, mutation.fetch туда же
+ * пишет (и await'ит запись — поэтому рефетч после invalidate гарантированно
+ * видит свежие данные). Optimistic-патчи дают мгновенный UI поверх этого.
+ */
+export const DB_NAME = 'kcal';
+
+export const FoodEntity = defineEntity({
+ name: 'food',
+ id: food => food.id,
+ storage: idbStore({ dbName: DB_NAME }),
+});
+
+export const EntryEntity = defineEntity({
+ name: 'entry',
+ id: entry => entry.id,
+ storage: idbStore({ dbName: DB_NAME }),
+});
+
+export const WeightEntity = defineEntity({
+ name: 'weight',
+ id: weight => weight.id,
+ storage: idbStore({ dbName: DB_NAME }),
+});
+
+export const ProfileEntity = defineEntity({
+ name: 'profile',
+ id: () => PROFILE_ID,
+ storage: idbStore({ dbName: DB_NAME }),
+});
+
+// defineEntity уже инстанцировал KeyedStore-ы — используем их как прямой доступ к idb.
+export const foodStore = FoodEntity.storage!;
+export const entryStore = EntryEntity.storage!;
+export const weightStore = WeightEntity.storage!;
+export const profileStore = ProfileEntity.storage!;
+
+// ── Queries ──────────────────────────────────────────────────────────────────
+
+export const foodsQuery = defineQuery({
+ name: 'foods.all',
+ key: () => ['foods'],
+ fetch: () => foodStore.readAll(),
+ normalize: items => ({
+ entities: { food: items },
+ result: { ids: [...items].sort((a, b) => a.name.localeCompare(b.name, 'ru')).map(f => f.id) },
+ }),
+ tags: () => ['foods'],
+});
+
+export const entriesByDayQuery = defineQuery<{ date: string }, Entry[], { ids: string[] }>({
+ name: 'entries.byDay',
+ key: args => ['entries', args.date],
+ fetch: async ({ date }) => (await entryStore.readAll()).filter(e => e.date === date),
+ normalize: items => ({
+ entities: { entry: items },
+ result: { ids: [...items].sort((a, b) => a.createdAt - b.createdAt).map(e => e.id) },
+ }),
+ tags: () => ['entries'],
+});
+
+export interface DaySummary {
+ date: string;
+ kcal: number;
+ protein: number;
+ fat: number;
+ carbs: number;
+ entries: number;
+}
+
+/** Агрегаты по дням для статистики; сущности в кэш не тянем. */
+export const daySummariesQuery = defineQuery({
+ name: 'stats.daySummaries',
+ key: () => ['stats', 'days'],
+ fetch: () => entryStore.readAll(),
+ normalize: (items) => {
+ const byDate = new Map();
+ for (const entry of items) {
+ let day = byDate.get(entry.date);
+ if (!day) {
+ day = { date: entry.date, kcal: 0, protein: 0, fat: 0, carbs: 0, entries: 0 };
+ byDate.set(entry.date, day);
+ }
+ day.kcal += entry.kcal;
+ day.protein += entry.protein;
+ day.fat += entry.fat;
+ day.carbs += entry.carbs;
+ day.entries += 1;
+ }
+ return { result: [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date)) };
+ },
+ tags: () => ['entries'],
+});
+
+export const weightsQuery = defineQuery({
+ name: 'weights.all',
+ key: () => ['weights'],
+ fetch: () => weightStore.readAll(),
+ normalize: items => ({
+ entities: { weight: items },
+ result: { ids: [...items].sort((a, b) => a.date.localeCompare(b.date)).map(w => w.id) },
+ }),
+ tags: () => ['weights'],
+});
+
+export const profileQuery = defineQuery({
+ name: 'profile.get',
+ key: () => ['profile'],
+ fetch: () => profileStore.read(PROFILE_ID),
+ normalize: profile => ({
+ entities: { profile: profile ? [profile] : [] },
+ result: { exists: profile !== undefined },
+ }),
+ tags: () => ['profile'],
+});
+
+// ── Mutations ────────────────────────────────────────────────────────────────
+
+export const addEntryMutation = defineMutation<{ entry: Entry }, Entry>({
+ name: 'entry.add',
+ fetch: async ({ entry }) => {
+ await entryStore.write([{ key: entry.id, value: entry }]);
+ if (entry.foodId) {
+ const food = await foodStore.read(entry.foodId);
+ if (food) {
+ await foodStore.write([{
+ key: food.id,
+ value: {
+ ...food,
+ usedCount: food.usedCount + 1,
+ lastUsedAt: entry.createdAt,
+ lastAmountG: entry.amountG ?? food.lastAmountG,
+ },
+ }]);
+ }
+ }
+ return entry;
+ },
+ optimistic: ({ entry }, ctx) => ctx.upsertEntity(EntryEntity, entry),
+ invalidate: () => ['entries', 'foods'],
+});
+
+export const updateEntryMutation = defineMutation<{ id: string; patch: Partial }, Entry | null>({
+ name: 'entry.update',
+ fetch: async ({ id, patch }) => {
+ const current = await entryStore.read(id);
+ if (!current) return null;
+ const next = { ...current, ...patch };
+ await entryStore.write([{ key: id, value: next }]);
+ return next;
+ },
+ optimistic: ({ id, patch }, ctx) => ctx.patchEntity(EntryEntity, id, patch),
+ invalidate: () => ['entries'],
+});
+
+export const removeEntryMutation = defineMutation<{ id: string }, string>({
+ name: 'entry.remove',
+ fetch: async ({ id }) => {
+ await entryStore.delete(id);
+ return id;
+ },
+ optimistic: ({ id }, ctx) => ctx.removeEntity(EntryEntity, id),
+ invalidate: () => ['entries'],
+});
+
+export const upsertFoodMutation = defineMutation<{ food: Food }, Food>({
+ name: 'food.upsert',
+ fetch: async ({ food }) => {
+ await foodStore.write([{ key: food.id, value: food }]);
+ return food;
+ },
+ optimistic: ({ food }, ctx) => ctx.upsertEntity(FoodEntity, food),
+ invalidate: () => ['foods'],
+});
+
+export const removeFoodMutation = defineMutation<{ id: string }, string>({
+ name: 'food.remove',
+ fetch: async ({ id }) => {
+ await foodStore.delete(id);
+ return id;
+ },
+ optimistic: ({ id }, ctx) => ctx.removeEntity(FoodEntity, id),
+ invalidate: () => ['foods'],
+});
+
+export const saveProfileMutation = defineMutation<{ profile: Profile }, Profile>({
+ name: 'profile.save',
+ fetch: async ({ profile }) => {
+ await profileStore.write([{ key: PROFILE_ID, value: profile }]);
+ return profile;
+ },
+ optimistic: ({ profile }, ctx) => ctx.upsertEntity(ProfileEntity, profile),
+ invalidate: () => ['profile'],
+});
+
+export const logWeightMutation = defineMutation<{ weight: WeightLog }, WeightLog>({
+ name: 'weight.log',
+ fetch: async ({ weight }) => {
+ await weightStore.write([{ key: weight.id, value: weight }]);
+ return weight;
+ },
+ optimistic: ({ weight }, ctx) => ctx.upsertEntity(WeightEntity, weight),
+ invalidate: () => ['weights'],
+});
+
+export const removeWeightMutation = defineMutation<{ id: string }, string>({
+ name: 'weight.remove',
+ fetch: async ({ id }) => {
+ await weightStore.delete(id);
+ return id;
+ },
+ optimistic: ({ id }, ctx) => ctx.removeEntity(WeightEntity, id),
+ invalidate: () => ['weights'],
+});
+
+export const allEntities = [FoodEntity, EntryEntity, WeightEntity, ProfileEntity];
+export const allQueries = [foodsQuery, entriesByDayQuery, daySummariesQuery, weightsQuery, profileQuery];
+export const allMutations = [
+ addEntryMutation,
+ updateEntryMutation,
+ removeEntryMutation,
+ upsertFoodMutation,
+ removeFoodMutation,
+ saveProfileMutation,
+ logWeightMutation,
+ removeWeightMutation,
+];
diff --git a/kcal/src/data/engine.ts b/kcal/src/data/engine.ts
new file mode 100644
index 0000000..a109923
--- /dev/null
+++ b/kcal/src/data/engine.ts
@@ -0,0 +1,29 @@
+import { createEngine, indexedDBAdapter } from 'vue-sync-engine';
+import { DB_NAME, allEntities, allMutations, allQueries, foodStore } from './defs';
+import { SEED_FOODS } from './seed';
+
+export const CACHE_DEFAULTS = {
+ // Локальные чтения стоят миллисекунды — рефетчим при каждой подписке,
+ // консистентность важнее экономии на readAll.
+ staleTime: 0,
+ gcTime: 10 * 60_000,
+};
+
+/** Наполняет каталог стартовым набором при первом запуске. */
+export async function seedIfEmpty(): Promise {
+ const existing = await foodStore.readAll();
+ if (existing.length > 0) return;
+ await foodStore.write(SEED_FOODS.map(food => ({ key: food.id, value: food })));
+}
+
+/** Inline-движок: QueryGraph и Mirror в одном треде, снапшоты запросов — в idb. */
+export function createKcalEngine() {
+ return createEngine({
+ entities: allEntities,
+ queries: allQueries,
+ mutations: allMutations,
+ storage: indexedDBAdapter({ dbName: DB_NAME }),
+ defaultStaleTime: CACHE_DEFAULTS.staleTime,
+ defaultGcTime: CACHE_DEFAULTS.gcTime,
+ });
+}
diff --git a/kcal/src/data/off.test.ts b/kcal/src/data/off.test.ts
new file mode 100644
index 0000000..dadbaa1
--- /dev/null
+++ b/kcal/src/data/off.test.ts
@@ -0,0 +1,71 @@
+import { describe, expect, it } from 'vitest';
+import { normalizeOffProduct } from './off';
+
+// Фикстуры — реальные ответы API от 2026-08-07 (curl, поля обрезаны до запрошенных).
+
+describe(normalizeOffProduct, () => {
+ it('barcode-API: Coca-Cola 5449000000996', () => {
+ const product = normalizeOffProduct({
+ code: '5449000000996',
+ brands: 'Coca-Cola',
+ product_name: 'coca-cola',
+ product_name_ru: 'Coca Cola',
+ serving_quantity: 330,
+ nutriments: {
+ 'energy-kcal_100g': 42,
+ energy_100g: 180,
+ carbohydrates_100g: 10.6,
+ fat_100g: 0,
+ },
+ });
+ expect(product).toEqual({
+ code: '5449000000996',
+ name: 'Coca Cola',
+ brand: 'Coca-Cola',
+ kcal: 42,
+ protein: 0,
+ fat: 0,
+ carbs: 10.6,
+ servingGrams: 330,
+ });
+ });
+
+ it('brands-массив и строковые числа', () => {
+ const product = normalizeOffProduct({
+ code: '5900951310935',
+ brands: ['Snickers'],
+ product_name_ru: 'Сникерс Тройной',
+ nutriments: { 'energy-kcal_100g': '435', proteins_100g: '0', fat_100g: 20.4, carbohydrates_100g: 53.3 },
+ });
+ expect(product?.brand).toBe('Snickers');
+ expect(product?.kcal).toBe(435);
+ expect(product?.fat).toBe(20.4);
+ });
+
+ it('без kcal, но с кДж — пересчитывает', () => {
+ const product = normalizeOffProduct({
+ code: '123',
+ product_name: 'Test',
+ nutriments: { energy_100g: 180 },
+ });
+ expect(product?.kcal).toBe(43); // 180 кДж / 4.184
+ });
+
+ it('запись без калорийности отбрасывается', () => {
+ expect(normalizeOffProduct({ code: '1', product_name: 'X', nutriments: {} })).toBeNull();
+ });
+
+ it('запись без имени отбрасывается', () => {
+ expect(normalizeOffProduct({ code: '1', nutriments: { 'energy-kcal_100g': 100 } })).toBeNull();
+ });
+
+ it('нереалистичный вес порции не попадает в servingGrams', () => {
+ const product = normalizeOffProduct({
+ code: '1',
+ product_name: 'X',
+ serving_quantity: 5000,
+ nutriments: { 'energy-kcal_100g': 100 },
+ });
+ expect(product?.servingGrams).toBeUndefined();
+ });
+});
diff --git a/kcal/src/data/off.ts b/kcal/src/data/off.ts
new file mode 100644
index 0000000..663c8a6
--- /dev/null
+++ b/kcal/src/data/off.ts
@@ -0,0 +1,89 @@
+import { round1 } from '../domain/calc';
+
+/**
+ * Open Food Facts — открытая база упакованных продуктов. Barcode-API отдаёт
+ * `Access-Control-Allow-Origin: *`, поэтому работает прямо из браузера без
+ * бэкенда. Текстовый поиск у OFF из браузера недоступен (legacy-эндпоинт
+ * отключён, у search-a-licious нет CORS), поэтому фича — только штрихкоды:
+ * сканер или цифры с упаковки.
+ */
+export interface OffProduct {
+ /** EAN/UPC штрихкод. */
+ code: string;
+ name: string;
+ brand?: string;
+ /** На 100 г. */
+ kcal: number;
+ protein: number;
+ fat: number;
+ carbs: number;
+ /** Вес порции/упаковки с этикетки, если указан. */
+ servingGrams?: number;
+}
+
+const FIELDS = 'code,product_name,product_name_ru,brands,nutriments,serving_quantity';
+
+export interface OffApiProduct {
+ code?: string;
+ product_name?: string;
+ product_name_ru?: string;
+ /** Barcode-API отдаёт строку через запятую, search-a-licious — массив. */
+ brands?: string | string[];
+ serving_quantity?: string | number;
+ nutriments?: Record;
+}
+
+function toNumber(value: string | number | undefined): number | null {
+ if (value === undefined) return null;
+ const parsed = Number(value);
+ return Number.isFinite(parsed) ? parsed : null;
+}
+
+/** kcal/100 г: предпочитаем готовое поле, иначе пересчёт из кДж. */
+function kcalPer100(nutriments: Record): number | null {
+ const direct = toNumber(nutriments['energy-kcal_100g']);
+ if (direct !== null && direct > 0) return direct;
+ const kj = toNumber(nutriments.energy_100g);
+ if (kj !== null && kj > 0) return kj / 4.184;
+ return null;
+}
+
+/** null — в записи нет калорийности или имени, для дневника она бесполезна. */
+export function normalizeOffProduct(raw: OffApiProduct): OffProduct | null {
+ const nutriments = raw.nutriments ?? {};
+ const kcal = kcalPer100(nutriments);
+ if (kcal === null || !raw.code) return null;
+ const name = (raw.product_name_ru || raw.product_name || '').trim();
+ if (name === '') return null;
+
+ const serving = toNumber(raw.serving_quantity);
+ const brandsRaw = Array.isArray(raw.brands) ? raw.brands[0] : raw.brands?.split(',')[0];
+ const brand = brandsRaw?.trim();
+ return {
+ code: raw.code,
+ name,
+ ...(brand ? { brand } : {}),
+ kcal: Math.round(kcal),
+ protein: round1(toNumber(nutriments.proteins_100g) ?? 0),
+ fat: round1(toNumber(nutriments.fat_100g) ?? 0),
+ carbs: round1(toNumber(nutriments.carbohydrates_100g) ?? 0),
+ ...(serving !== null && serving >= 1 && serving <= 1500 ? { servingGrams: serving } : {}),
+ };
+}
+
+/** Продукт по штрихкоду; null — штрихкода нет в базе. */
+export async function fetchOffByBarcode(barcode: string): Promise {
+ const url = `https://world.openfoodfacts.org/api/v2/product/${encodeURIComponent(barcode)}.json?fields=${FIELDS}`;
+ let response: Response;
+ try {
+ response = await fetch(url);
+ }
+ catch {
+ throw new Error('Нет соединения с базой — проверьте интернет.');
+ }
+ if (response.status === 404) return null;
+ if (!response.ok) throw new Error(`База недоступна (${response.status})`);
+ const data = await response.json() as { status?: number; product?: OffApiProduct };
+ if (!data.product || data.status === 0) return null;
+ return normalizeOffProduct({ ...data.product, code: data.product.code ?? barcode });
+}
diff --git a/kcal/src/data/seed.ts b/kcal/src/data/seed.ts
new file mode 100644
index 0000000..050ed37
--- /dev/null
+++ b/kcal/src/data/seed.ts
@@ -0,0 +1,100 @@
+import type { Food } from '../domain/types';
+
+type SeedFood = Pick;
+
+/**
+ * Стартовый каталог: типовые продукты по таблицам пищевой ценности, на 100 г.
+ * Значения округлены — для дневника важна стабильность оценки, а не лабораторная
+ * точность. Всё редактируется как свой продукт.
+ */
+const SEED: readonly SeedFood[] = [
+ // Крупы и гарниры (в готовом виде — так, как еда попадает на весы)
+ { category: 'Крупы и гарниры', name: 'Гречка варёная', kcal: 110, protein: 4.2, fat: 1.1, carbs: 21.3 },
+ { category: 'Крупы и гарниры', name: 'Рис варёный', kcal: 130, protein: 2.4, fat: 0.4, carbs: 28.6 },
+ { category: 'Крупы и гарниры', name: 'Булгур варёный', kcal: 83, protein: 3.1, fat: 0.2, carbs: 18.6 },
+ { category: 'Крупы и гарниры', name: 'Овсянка на воде', kcal: 88, protein: 3, fat: 1.7, carbs: 15 },
+ { category: 'Крупы и гарниры', name: 'Овсянка на молоке', kcal: 102, protein: 4.1, fat: 3.2, carbs: 14.2 },
+ { category: 'Крупы и гарниры', name: 'Макароны варёные', kcal: 155, protein: 5.3, fat: 0.9, carbs: 30.9 },
+ { category: 'Крупы и гарниры', name: 'Картофель варёный', kcal: 82, protein: 2, fat: 0.4, carbs: 17 },
+ { category: 'Крупы и гарниры', name: 'Картофельное пюре', kcal: 106, protein: 2.2, fat: 3.3, carbs: 16.7 },
+ { category: 'Крупы и гарниры', name: 'Картофель жареный', kcal: 192, protein: 2.8, fat: 9.5, carbs: 23.4 },
+
+ // Мясо, птица, рыба (приготовленные)
+ { category: 'Мясо и рыба', name: 'Куриная грудка запечённая', kcal: 165, protein: 31, fat: 3.6, carbs: 0 },
+ { category: 'Мясо и рыба', name: 'Куриное бедро без кожи', kcal: 195, protein: 24.4, fat: 10.9, carbs: 0 },
+ { category: 'Мясо и рыба', name: 'Котлета куриная жареная', kcal: 222, protein: 18, fat: 14, carbs: 6.5 },
+ { category: 'Мясо и рыба', name: 'Говядина тушёная', kcal: 232, protein: 25.8, fat: 14.2, carbs: 0 },
+ { category: 'Мясо и рыба', name: 'Свинина запечённая', kcal: 271, protein: 25, fat: 19, carbs: 0 },
+ { category: 'Мясо и рыба', name: 'Лосось запечённый', kcal: 208, protein: 22, fat: 13, carbs: 0 },
+ { category: 'Мясо и рыба', name: 'Треска запечённая', kcal: 90, protein: 20, fat: 0.9, carbs: 0 },
+ { category: 'Мясо и рыба', name: 'Тунец консервированный', kcal: 116, protein: 25.5, fat: 1, carbs: 0.2 },
+ { category: 'Мясо и рыба', name: 'Креветки варёные', kcal: 99, protein: 21, fat: 1.2, carbs: 0.2 },
+ { category: 'Мясо и рыба', name: 'Сосиска молочная', kcal: 261, protein: 11, fat: 23.9, carbs: 1.6, pieceGrams: 50 },
+ { category: 'Мясо и рыба', name: 'Колбаса докторская', kcal: 257, protein: 13.7, fat: 22.8, carbs: 0 },
+
+ // Молочное и яйца
+ { category: 'Молочное и яйца', name: 'Яйцо куриное', kcal: 157, protein: 12.7, fat: 11.5, carbs: 0.7, pieceGrams: 55 },
+ { category: 'Молочное и яйца', name: 'Творог 5%', kcal: 121, protein: 17.2, fat: 5, carbs: 1.8 },
+ { category: 'Молочное и яйца', name: 'Творог 9%', kcal: 159, protein: 16.7, fat: 9, carbs: 2 },
+ { category: 'Молочное и яйца', name: 'Сыр твёрдый', kcal: 364, protein: 23.2, fat: 29.5, carbs: 0.3 },
+ { category: 'Молочное и яйца', name: 'Молоко 2,5%', kcal: 52, protein: 2.8, fat: 2.5, carbs: 4.7 },
+ { category: 'Молочное и яйца', name: 'Кефир 2,5%', kcal: 50, protein: 2.8, fat: 2.5, carbs: 3.9 },
+ { category: 'Молочное и яйца', name: 'Йогурт греческий 2%', kcal: 66, protein: 8, fat: 2, carbs: 3.5 },
+ { category: 'Молочное и яйца', name: 'Сметана 15%', kcal: 158, protein: 2.6, fat: 15, carbs: 3 },
+ { category: 'Молочное и яйца', name: 'Масло сливочное', kcal: 748, protein: 0.5, fat: 82.5, carbs: 0.8 },
+
+ // Овощи и фрукты
+ { category: 'Овощи и фрукты', name: 'Огурец', kcal: 15, protein: 0.8, fat: 0.1, carbs: 2.8 },
+ { category: 'Овощи и фрукты', name: 'Помидор', kcal: 20, protein: 1.1, fat: 0.2, carbs: 3.7 },
+ { category: 'Овощи и фрукты', name: 'Капуста белокочанная', kcal: 28, protein: 1.8, fat: 0.2, carbs: 4.7 },
+ { category: 'Овощи и фрукты', name: 'Морковь', kcal: 35, protein: 1.3, fat: 0.1, carbs: 6.9 },
+ { category: 'Овощи и фрукты', name: 'Салат овощной с маслом', kcal: 90, protein: 1.2, fat: 7, carbs: 5.5 },
+ { category: 'Овощи и фрукты', name: 'Банан', kcal: 96, protein: 1.5, fat: 0.2, carbs: 21.8, pieceGrams: 120 },
+ { category: 'Овощи и фрукты', name: 'Яблоко', kcal: 47, protein: 0.4, fat: 0.4, carbs: 9.8, pieceGrams: 180 },
+ { category: 'Овощи и фрукты', name: 'Апельсин', kcal: 43, protein: 0.9, fat: 0.2, carbs: 8.1, pieceGrams: 150 },
+ { category: 'Овощи и фрукты', name: 'Авокадо', kcal: 160, protein: 2, fat: 14.7, carbs: 8.5 },
+ { category: 'Овощи и фрукты', name: 'Виноград', kcal: 72, protein: 0.6, fat: 0.6, carbs: 15.4 },
+
+ // Хлеб и выпечка
+ { category: 'Хлеб и выпечка', name: 'Хлеб белый', kcal: 265, protein: 8.1, fat: 3.2, carbs: 50.1, pieceGrams: 25 },
+ { category: 'Хлеб и выпечка', name: 'Хлеб ржаной', kcal: 210, protein: 6.6, fat: 1.2, carbs: 41.4, pieceGrams: 30 },
+ { category: 'Хлеб и выпечка', name: 'Лаваш тонкий', kcal: 275, protein: 9.1, fat: 1.1, carbs: 56 },
+ { category: 'Хлеб и выпечка', name: 'Печенье овсяное', kcal: 437, protein: 6.5, fat: 14.4, carbs: 71.8, pieceGrams: 20 },
+
+ // Готовые блюда
+ { category: 'Готовые блюда', name: 'Борщ', kcal: 49, protein: 1.6, fat: 2.2, carbs: 5.5 },
+ { category: 'Готовые блюда', name: 'Суп куриный с лапшой', kcal: 68, protein: 3.9, fat: 2.1, carbs: 8.2 },
+ { category: 'Готовые блюда', name: 'Плов с курицей', kcal: 190, protein: 9.5, fat: 7.5, carbs: 21 },
+ { category: 'Готовые блюда', name: 'Пельмени варёные', kcal: 275, protein: 11.9, fat: 12.4, carbs: 29 },
+ { category: 'Готовые блюда', name: 'Пицца', kcal: 266, protein: 11, fat: 10.4, carbs: 32.9, pieceGrams: 120 },
+ { category: 'Готовые блюда', name: 'Сырники жареные', kcal: 220, protein: 15.5, fat: 9.5, carbs: 18.2, pieceGrams: 75 },
+ { category: 'Готовые блюда', name: 'Блины', kcal: 233, protein: 6.1, fat: 12.3, carbs: 26, pieceGrams: 50 },
+
+ // Орехи и сладкое
+ { category: 'Орехи и сладкое', name: 'Грецкий орех', kcal: 654, protein: 15.2, fat: 65.2, carbs: 7 },
+ { category: 'Орехи и сладкое', name: 'Миндаль', kcal: 609, protein: 18.6, fat: 53.7, carbs: 13 },
+ { category: 'Орехи и сладкое', name: 'Арахисовая паста', kcal: 588, protein: 25, fat: 50, carbs: 20 },
+ { category: 'Орехи и сладкое', name: 'Шоколад молочный', kcal: 554, protein: 9.8, fat: 34.7, carbs: 50.4, pieceGrams: 6 },
+ { category: 'Орехи и сладкое', name: 'Мёд', kcal: 329, protein: 0.8, fat: 0, carbs: 81.5 },
+ { category: 'Орехи и сладкое', name: 'Сахар', kcal: 398, protein: 0, fat: 0, carbs: 99.7, pieceGrams: 5 },
+ { category: 'Орехи и сладкое', name: 'Мороженое пломбир', kcal: 227, protein: 3.2, fat: 15, carbs: 20.8 },
+ { category: 'Орехи и сладкое', name: 'Чипсы картофельные', kcal: 536, protein: 5.5, fat: 34, carbs: 51 },
+
+ // Напитки
+ { category: 'Напитки', name: 'Кола', kcal: 42, protein: 0, fat: 0, carbs: 10.6 },
+ { category: 'Напитки', name: 'Сок яблочный', kcal: 46, protein: 0.5, fat: 0.1, carbs: 10.1 },
+ { category: 'Напитки', name: 'Капучино', kcal: 45, protein: 2.1, fat: 2.3, carbs: 4, pieceGrams: 200 },
+ { category: 'Напитки', name: 'Латте', kcal: 47, protein: 2.4, fat: 2.4, carbs: 4.2, pieceGrams: 300 },
+ { category: 'Напитки', name: 'Пиво светлое', kcal: 42, protein: 0.5, fat: 0, carbs: 3.5 },
+ { category: 'Напитки', name: 'Вино сухое', kcal: 68, protein: 0.2, fat: 0, carbs: 2.6 },
+];
+
+/** Стабильные id — сидинг идемпотентен и не плодит дубликатов. */
+export const SEED_FOODS: readonly Food[] = SEED.map((item, index) => ({
+ ...item,
+ id: `seed-${String(index + 1).padStart(3, '0')}`,
+ builtin: true,
+ usedCount: 0,
+ lastUsedAt: 0,
+ createdAt: 0,
+}));
diff --git a/kcal/src/domain/calc.test.ts b/kcal/src/domain/calc.test.ts
new file mode 100644
index 0000000..f34648f
--- /dev/null
+++ b/kcal/src/domain/calc.test.ts
@@ -0,0 +1,76 @@
+import { describe, expect, it } from 'vitest';
+import { bmr, computeTargets, portionNutrients, safeKcalFloor, sumNutrients, tdee } from './calc';
+
+describe('bmr (Миффлин—Сан Жеор)', () => {
+ it('мужчина 30 лет, 180 см, 80 кг', () => {
+ // 10*80 + 6.25*180 - 5*30 + 5 = 800 + 1125 - 150 + 5
+ expect(bmr('male', 30, 180, 80)).toBe(1780);
+ });
+
+ it('женщина 25 лет, 165 см, 60 кг', () => {
+ // 10*60 + 6.25*165 - 5*25 - 161 = 600 + 1031.25 - 125 - 161
+ expect(bmr('female', 25, 165, 60)).toBeCloseTo(1345.25, 2);
+ });
+});
+
+describe(tdee, () => {
+ it('умножает BMR на коэффициент активности', () => {
+ expect(tdee('male', 30, 180, 80, 1.55)).toBeCloseTo(1780 * 1.55, 5);
+ });
+});
+
+describe(computeTargets, () => {
+ const params = { sex: 'male' as const, age: 30, heightCm: 180, weightKg: 80, activity: 1.55 };
+
+ it('похудение: дефицит 15%, белок 1.8 г/кг, жир 0.9 г/кг', () => {
+ const t = computeTargets({ ...params, goal: 'lose' });
+ expect(t.kcal).toBe(2350); // 2759 * 0.85 = 2345.15 → к ближайшим 10
+ expect(t.protein).toBe(144);
+ expect(t.fat).toBe(72);
+ // Углеводы добираются из остатка: (2350 - 144*4 - 72*9) / 4
+ expect(t.carbs).toBe(Math.round((2350 - 144 * 4 - 72 * 9) / 4));
+ });
+
+ it('поддержание: без дефицита', () => {
+ const t = computeTargets({ ...params, goal: 'maintain' });
+ expect(t.kcal).toBe(2760);
+ expect(t.protein).toBe(128);
+ });
+
+ it('набор: профицит 10%', () => {
+ const t = computeTargets({ ...params, goal: 'gain' });
+ expect(t.kcal).toBe(3030); // 2759 * 1.1 = 3034.9 → к ближайшим 10
+ });
+
+ it('углеводы не уходят в минус на экстремальных входных', () => {
+ const t = computeTargets({ sex: 'female', age: 70, heightCm: 150, weightKg: 45, activity: 1.2, goal: 'lose' });
+ expect(t.carbs).toBeGreaterThanOrEqual(0);
+ expect(t.kcal).toBeLessThan(safeKcalFloor('female') + 500);
+ });
+});
+
+describe(portionNutrients, () => {
+ const buckwheat = { kcal: 110, protein: 4.2, fat: 1.1, carbs: 21.3 };
+
+ it('масштабирует значения на 100 г к порции', () => {
+ expect(portionNutrients(buckwheat, 250)).toEqual({ kcal: 275, protein: 10.5, fat: 2.8, carbs: 53.3 });
+ });
+
+ it('порция 0 г — нули', () => {
+ expect(portionNutrients(buckwheat, 0)).toEqual({ kcal: 0, protein: 0, fat: 0, carbs: 0 });
+ });
+});
+
+describe(sumNutrients, () => {
+ it('складывает записи и округляет макросы до десятых', () => {
+ const total = sumNutrients([
+ { kcal: 275, protein: 10.5, fat: 2.8, carbs: 53.3 },
+ { kcal: 157, protein: 12.7, fat: 11.5, carbs: 0.7 },
+ ]);
+ expect(total).toEqual({ kcal: 432, protein: 23.2, fat: 14.3, carbs: 54 });
+ });
+
+ it('пустой список — нули', () => {
+ expect(sumNutrients([])).toEqual({ kcal: 0, protein: 0, fat: 0, carbs: 0 });
+ });
+});
diff --git a/kcal/src/domain/calc.ts b/kcal/src/domain/calc.ts
new file mode 100644
index 0000000..f6691a1
--- /dev/null
+++ b/kcal/src/domain/calc.ts
@@ -0,0 +1,86 @@
+import type { Food, Goal, Nutrients, Sex } from './types';
+
+/** Базовый обмен по Миффлину—Сан Жеору, ккал/сутки. */
+export function bmr(sex: Sex, age: number, heightCm: number, weightKg: number): number {
+ const base = 10 * weightKg + 6.25 * heightCm - 5 * age;
+ return sex === 'male' ? base + 5 : base - 161;
+}
+
+/** Суточный расход с учётом активности. */
+export function tdee(sex: Sex, age: number, heightCm: number, weightKg: number, activity: number): number {
+ return bmr(sex, age, heightCm, weightKg) * activity;
+}
+
+/**
+ * Пресеты целей: множитель калорийности от TDEE и нормы белка/жира на кг веса.
+ * Похудение — умеренный дефицит 15%, белок повышен для сохранения мышц;
+ * набор — профицит 10%. Углеводы добираются из остатка калорий.
+ */
+const GOAL_PRESETS: Record = {
+ lose: { kcalFactor: 0.85, proteinPerKg: 1.8, fatPerKg: 0.9 },
+ maintain: { kcalFactor: 1.0, proteinPerKg: 1.6, fatPerKg: 1.0 },
+ gain: { kcalFactor: 1.1, proteinPerKg: 1.8, fatPerKg: 1.0 },
+};
+
+/** Безопасный минимум калорийности; ниже — предупреждаем, но не запрещаем. */
+export function safeKcalFloor(sex: Sex): number {
+ return sex === 'male' ? 1500 : 1200;
+}
+
+export interface TargetInput {
+ sex: Sex;
+ age: number;
+ heightCm: number;
+ weightKg: number;
+ activity: number;
+ goal: Goal;
+}
+
+/** Дневные цели по калориям и БЖУ из параметров тела и цели. */
+export function computeTargets(input: TargetInput): Nutrients {
+ const preset = GOAL_PRESETS[input.goal];
+ const kcal = roundTo(tdee(input.sex, input.age, input.heightCm, input.weightKg, input.activity) * preset.kcalFactor, 10);
+ const protein = Math.round(input.weightKg * preset.proteinPerKg);
+ const fat = Math.round(input.weightKg * preset.fatPerKg);
+ const carbs = Math.max(0, Math.round((kcal - protein * 4 - fat * 9) / 4));
+ return { kcal, protein, fat, carbs };
+}
+
+/** Нутриенты порции продукта: значения каталога даны на 100 г. */
+export function portionNutrients(food: Nutrients, amountG: number): Nutrients {
+ const k = amountG / 100;
+ return {
+ kcal: Math.round(food.kcal * k),
+ protein: round1(food.protein * k),
+ fat: round1(food.fat * k),
+ carbs: round1(food.carbs * k),
+ };
+}
+
+/** Сумма нутриентов по записям. */
+export function sumNutrients(items: readonly Nutrients[]): Nutrients {
+ const total = { kcal: 0, protein: 0, fat: 0, carbs: 0 };
+ for (const item of items) {
+ total.kcal += item.kcal;
+ total.protein += item.protein;
+ total.fat += item.fat;
+ total.carbs += item.carbs;
+ }
+ total.protein = round1(total.protein);
+ total.fat = round1(total.fat);
+ total.carbs = round1(total.carbs);
+ return total;
+}
+
+/** Порция по умолчанию для продукта: прошлая → одна штука → 100 г. */
+export function defaultAmount(food: Food): number {
+ return food.lastAmountG ?? food.pieceGrams ?? 100;
+}
+
+export function round1(value: number): number {
+ return Math.round(value * 10) / 10;
+}
+
+export function roundTo(value: number, step: number): number {
+ return Math.round(value / step) * step;
+}
diff --git a/kcal/src/domain/dates.test.ts b/kcal/src/domain/dates.test.ts
new file mode 100644
index 0000000..f8b13c6
--- /dev/null
+++ b/kcal/src/domain/dates.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from 'vitest';
+import { dayTitle, lastDays, shiftISODate, toISODate } from './dates';
+
+describe(toISODate, () => {
+ it('локальная дата без сдвига в UTC', () => {
+ expect(toISODate(new Date(2026, 7, 7))).toBe('2026-08-07');
+ expect(toISODate(new Date(2026, 0, 1, 0, 30))).toBe('2026-01-01');
+ });
+});
+
+describe(shiftISODate, () => {
+ it('переход через границу месяца и года', () => {
+ expect(shiftISODate('2026-08-01', -1)).toBe('2026-07-31');
+ expect(shiftISODate('2025-12-31', 1)).toBe('2026-01-01');
+ expect(shiftISODate('2026-08-07', 0)).toBe('2026-08-07');
+ });
+
+ it('високальный февраль', () => {
+ expect(shiftISODate('2028-02-28', 1)).toBe('2028-02-29');
+ });
+});
+
+describe(dayTitle, () => {
+ const today = '2026-08-07';
+
+ it('относительные дни', () => {
+ expect(dayTitle('2026-08-07', today)).toBe('Сегодня');
+ expect(dayTitle('2026-08-06', today)).toBe('Вчера');
+ expect(dayTitle('2026-08-08', today)).toBe('Завтра');
+ });
+
+ it('дальние дни — день недели и число по-русски', () => {
+ expect(dayTitle('2026-08-01', today)).toMatch(/1 августа/);
+ });
+});
+
+describe(lastDays, () => {
+ it('последние N дней по возрастанию, включая сегодня', () => {
+ expect(lastDays(3, '2026-08-07')).toEqual(['2026-08-05', '2026-08-06', '2026-08-07']);
+ });
+});
diff --git a/kcal/src/domain/dates.ts b/kcal/src/domain/dates.ts
new file mode 100644
index 0000000..813a92c
--- /dev/null
+++ b/kcal/src/domain/dates.ts
@@ -0,0 +1,41 @@
+import { addDays, format, parseISO } from 'date-fns';
+import { ru } from 'date-fns/locale';
+
+/** Локальная дата в формате YYYY-MM-DD (не UTC — день дневника «человеческий»). */
+export function toISODate(date: Date): string {
+ return format(date, 'yyyy-MM-dd');
+}
+
+export function todayISO(): string {
+ return toISODate(new Date());
+}
+
+/** Локальная полночь указанного дня (date-fns трактует date-only строки как локальные). */
+export function parseISODate(iso: string): Date {
+ return parseISO(iso);
+}
+
+export function shiftISODate(iso: string, days: number): string {
+ return toISODate(addDays(parseISODate(iso), days));
+}
+
+/** «Сегодня» / «Вчера» / «сб, 1 августа» — ru-локаль даёт родительный падеж месяца. */
+export function dayTitle(iso: string, today: string = todayISO()): string {
+ if (iso === today) return 'Сегодня';
+ if (iso === shiftISODate(today, -1)) return 'Вчера';
+ if (iso === shiftISODate(today, 1)) return 'Завтра';
+ return format(parseISODate(iso), 'EEEEEE, d MMMM', { locale: ru });
+}
+
+/** Короткая подпись дня для графиков: «5.08». */
+export function dayShort(iso: string): string {
+ return format(parseISODate(iso), 'd.MM');
+}
+
+/** Последние `count` дней, включая сегодняшний, по возрастанию. */
+export function lastDays(count: number, today: string = todayISO()): string[] {
+ const end = parseISODate(today);
+ const days: string[] = [];
+ for (let i = count - 1; i >= 0; i--) days.push(toISODate(addDays(end, -i)));
+ return days;
+}
diff --git a/kcal/src/domain/format.ts b/kcal/src/domain/format.ts
new file mode 100644
index 0000000..a99a0e5
--- /dev/null
+++ b/kcal/src/domain/format.ts
@@ -0,0 +1,22 @@
+const kcalFormat = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 0 });
+const gramFormat = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 1 });
+
+/** «1 840» — целые ккал с неразрывной группировкой. */
+export function fmtKcal(value: number): string {
+ return kcalFormat.format(Math.round(value));
+}
+
+/** «82,5» — граммы с одним знаком, без хвоста «,0». */
+export function fmtG(value: number): string {
+ return gramFormat.format(value);
+}
+
+/** Подпись порции записи: «150 г» или «2 шт · 110 г». */
+export function fmtAmount(amountG: number | undefined, pieceGrams: number | undefined): string {
+ if (amountG === undefined) return 'порция';
+ if (pieceGrams && amountG % pieceGrams === 0) {
+ const pieces = amountG / pieceGrams;
+ return pieces === 1 ? `1 шт · ${fmtG(amountG)} г` : `${pieces} шт · ${fmtG(amountG)} г`;
+ }
+ return `${fmtG(amountG)} г`;
+}
diff --git a/kcal/src/domain/types.ts b/kcal/src/domain/types.ts
new file mode 100644
index 0000000..213fa0e
--- /dev/null
+++ b/kcal/src/domain/types.ts
@@ -0,0 +1,102 @@
+/** Приём пищи, к которому привязана запись дневника. */
+export type Meal = 'breakfast' | 'lunch' | 'dinner' | 'snack';
+
+export const MEALS: readonly Meal[] = ['breakfast', 'lunch', 'dinner', 'snack'];
+
+export const MEAL_LABELS: Record = {
+ breakfast: 'Завтрак',
+ lunch: 'Обед',
+ dinner: 'Ужин',
+ snack: 'Перекус',
+};
+
+/** Пищевая ценность. Для Food — на 100 г, для Entry — на порцию целиком. */
+export interface Nutrients {
+ kcal: number;
+ protein: number;
+ fat: number;
+ carbs: number;
+}
+
+/** Продукт личного каталога. Все значения — на 100 г. */
+export interface Food extends Nutrients {
+ id: string;
+ name: string;
+ category: string;
+ /** Вес одной штуки в граммах — включает режим «в штуках» при вводе порции. */
+ pieceGrams?: number;
+ /** EAN/UPC с упаковки — для дедупликации при повторном сканировании. */
+ barcode?: string;
+ /** Продукт из стартового набора (можно редактировать как свой). */
+ builtin?: boolean;
+ /** Сколько раз добавляли в дневник — для сортировки «недавних». */
+ usedCount: number;
+ /** Момент последнего добавления в дневник. */
+ lastUsedAt: number;
+ /** Последняя введённая порция в граммах — подставляется по умолчанию. */
+ lastAmountG?: number;
+ createdAt: number;
+}
+
+/**
+ * Запись дневника. Хранит снапшот имени и итоговых нутриентов на порцию:
+ * правка или удаление продукта из каталога не меняет историю.
+ */
+export interface Entry extends Nutrients {
+ id: string;
+ /** Локальная дата дня дневника в формате YYYY-MM-DD. */
+ date: string;
+ meal: Meal;
+ /** Ссылка на продукт каталога; отсутствует у быстрых записей «только ккал». */
+ foodId?: string;
+ name: string;
+ /** Размер порции в граммах; отсутствует у быстрых записей. */
+ amountG?: number;
+ createdAt: number;
+}
+
+/** Замер веса; один на день, id совпадает с датой. */
+export interface WeightLog {
+ id: string;
+ date: string;
+ kg: number;
+ createdAt: number;
+}
+
+export type Sex = 'male' | 'female';
+export type Goal = 'lose' | 'maintain' | 'gain';
+
+/** Коэффициенты активности к BMR (множители Харриса—Бенедикта). */
+export const ACTIVITY_LEVELS = [
+ { value: 1.2, label: 'Минимальная', hint: 'сидячая работа, без тренировок' },
+ { value: 1.375, label: 'Лёгкая', hint: '1–3 тренировки в неделю' },
+ { value: 1.55, label: 'Средняя', hint: '3–5 тренировок в неделю' },
+ { value: 1.725, label: 'Высокая', hint: '6–7 тренировок в неделю' },
+ { value: 1.9, label: 'Экстремальная', hint: 'физический труд + тренировки' },
+] as const;
+
+export const GOAL_LABELS: Record = {
+ lose: 'Похудение',
+ maintain: 'Поддержание',
+ gain: 'Набор массы',
+};
+
+/** Профиль пользователя — единственная запись с id = 'profile'. */
+export interface Profile {
+ id: 'profile';
+ sex: Sex;
+ age: number;
+ heightCm: number;
+ weightKg: number;
+ activity: number;
+ goal: Goal;
+ /** Дневные цели. Пересчитываются из параметров, но правятся и вручную. */
+ targetKcal: number;
+ targetProtein: number;
+ targetFat: number;
+ targetCarbs: number;
+ createdAt: number;
+ updatedAt: number;
+}
+
+export const PROFILE_ID = 'profile' as const;
diff --git a/kcal/src/env.d.ts b/kcal/src/env.d.ts
new file mode 100644
index 0000000..38706d6
--- /dev/null
+++ b/kcal/src/env.d.ts
@@ -0,0 +1,13 @@
+///
+
+// BarcodeDetector (Chromium): в lib.dom его ещё нет — минимальная декларация.
+interface DetectedBarcode {
+ rawValue: string;
+ format: string;
+}
+
+declare class BarcodeDetector {
+ constructor(options?: { formats?: string[] });
+ detect(source: CanvasImageSource): Promise;
+ static getSupportedFormats(): Promise;
+}
diff --git a/kcal/src/main.ts b/kcal/src/main.ts
new file mode 100644
index 0000000..a3f23fa
--- /dev/null
+++ b/kcal/src/main.ts
@@ -0,0 +1,20 @@
+import '@fontsource-variable/golos-text';
+import '@fontsource/spectral/300.css';
+import '@fontsource/spectral/500.css';
+import './app.css';
+import { createVaporApp } from 'vue';
+import type { App as VueApp } from 'vue';
+import { installEngine } from 'vue-sync-engine';
+import App from './App';
+import { CACHE_DEFAULTS, createKcalEngine, seedIfEmpty } from './data/engine';
+
+// Стартовый каталог должен лечь в idb до первой подписки на foods-запрос.
+await seedIfEmpty();
+
+const engine = createKcalEngine();
+const app = createVaporApp(App);
+
+// installEngine типизирован под vdom-App; VaporApp имеет совместимые provide/config.
+installEngine(app as unknown as VueApp, engine, { defaults: CACHE_DEFAULTS });
+
+app.mount('#app');
diff --git a/kcal/src/screens/AddSheet.tsx b/kcal/src/screens/AddSheet.tsx
new file mode 100644
index 0000000..83811d5
--- /dev/null
+++ b/kcal/src/screens/AddSheet.tsx
@@ -0,0 +1,607 @@
+import { computed, onMounted, shallowRef } from 'vue';
+import { useRef } from 'vue-jsx-vapor';
+import { useMutation, useQuery } from 'vue-sync-engine';
+import { useCloseWatcher } from '@robonen/vue';
+import { groupBy } from '@robonen/stdlib';
+import { focus } from '@robonen/platform/browsers';
+import { FoodEntity, addEntryMutation, foodsQuery, upsertFoodMutation } from '../data/defs';
+import { useEntities } from '../data/composables';
+import { fetchOffByBarcode } from '../data/off';
+import type { OffProduct } from '../data/off';
+import { defaultAmount, portionNutrients } from '../domain/calc';
+import { fmtG, fmtKcal } from '../domain/format';
+import { MEALS, MEAL_LABELS } from '../domain/types';
+import type { Entry, Food, Meal } from '../domain/types';
+import { addSheet, selectedDate } from '../ui/state';
+import { IconBarcode, IconChevronLeft, IconClose, IconSearch } from '../ui/icons';
+import BarcodeScanner, { isBarcodeScanSupported } from '../components/BarcodeScanner';
+
+type Step = 'pick' | 'amount' | 'quick' | 'food';
+
+const GRAM_PRESETS = [50, 100, 150, 200, 300];
+
+export default function AddSheet() {
+ const step = shallowRef('pick');
+ const query = shallowRef('');
+ const meal = shallowRef(addSheet.meal);
+ const searchEl = useRef();
+
+ const foodsQ = useQuery(foodsQuery, () => undefined);
+ const foods = useEntities(FoodEntity, () => foodsQ.data.value?.ids);
+
+ const addEntry = useMutation(addEntryMutation);
+ const upsertFood = useMutation(upsertFoodMutation);
+
+ const close = () => (addSheet.open = false);
+ useCloseWatcher().onClose(close);
+ onMounted(() => focus(searchEl.value as HTMLElement | null));
+
+ // ── выбор продукта ────────────────────────────────────────────────────────
+ const trimmed = computed(() => query.value.trim().toLowerCase());
+ const filtered = computed(() =>
+ trimmed.value === ''
+ ? foods.value
+ : foods.value.filter(food => food.name.toLowerCase().includes(trimmed.value)),
+ );
+ const recents = computed(() =>
+ [...foods.value]
+ .filter(food => food.usedCount > 0)
+ .sort((a, b) => b.lastUsedAt - a.lastUsedAt)
+ .slice(0, 8),
+ );
+ const groups = computed(() => {
+ const grouped = groupBy(filtered.value, food => food.category);
+ return Object.entries(grouped).sort((a, b) => a[0].localeCompare(b[0], 'ru'));
+ });
+
+ // ── порция ────────────────────────────────────────────────────────────────
+ const selectedFood = shallowRef(null);
+ const amountG = shallowRef(100);
+
+ const pickFood = (food: Food) => {
+ selectedFood.value = food;
+ amountG.value = defaultAmount(food);
+ step.value = 'amount';
+ };
+
+ const preview = computed(() =>
+ selectedFood.value ? portionNutrients(selectedFood.value, amountG.value) : null,
+ );
+
+ const submitAmount = () => {
+ const food = selectedFood.value;
+ if (!food || amountG.value <= 0) return;
+ const entry: Entry = {
+ id: crypto.randomUUID(),
+ date: selectedDate.value,
+ meal: meal.value,
+ foodId: food.id,
+ name: food.name,
+ amountG: amountG.value,
+ ...portionNutrients(food, amountG.value),
+ createdAt: Date.now(),
+ };
+ addEntry.mutate({ entry });
+ close();
+ };
+
+ // ── быстрая запись ────────────────────────────────────────────────────────
+ const quickName = shallowRef('');
+ const quickKcal = shallowRef(0);
+ const quickProtein = shallowRef(0);
+ const quickFat = shallowRef(0);
+ const quickCarbs = shallowRef(0);
+
+ const submitQuick = () => {
+ if (quickKcal.value <= 0) return;
+ const entry: Entry = {
+ id: crypto.randomUUID(),
+ date: selectedDate.value,
+ meal: meal.value,
+ name: quickName.value.trim() || 'Быстрая запись',
+ kcal: Math.round(quickKcal.value),
+ protein: quickProtein.value,
+ fat: quickFat.value,
+ carbs: quickCarbs.value,
+ createdAt: Date.now(),
+ };
+ addEntry.mutate({ entry });
+ close();
+ };
+
+ // ── новый продукт ─────────────────────────────────────────────────────────
+ const foodName = shallowRef('');
+ const foodCategory = shallowRef('Моё');
+ const foodKcal = shallowRef(0);
+ const foodProtein = shallowRef(0);
+ const foodFat = shallowRef(0);
+ const foodCarbs = shallowRef(0);
+ const foodPiece = shallowRef(0);
+ const foodBarcode = shallowRef('');
+ const categories = computed(() => [...new Set(foods.value.map(food => food.category))].sort((a, b) => a.localeCompare(b, 'ru')));
+
+ const submitFood = async () => {
+ if (foodName.value.trim() === '' || foodKcal.value <= 0) return;
+ const food: Food = {
+ id: crypto.randomUUID(),
+ name: foodName.value.trim(),
+ category: foodCategory.value.trim() || 'Моё',
+ kcal: foodKcal.value,
+ protein: foodProtein.value,
+ fat: foodFat.value,
+ carbs: foodCarbs.value,
+ ...(foodPiece.value > 0 ? { pieceGrams: foodPiece.value } : {}),
+ ...(foodBarcode.value !== '' ? { barcode: foodBarcode.value } : {}),
+ usedCount: 0,
+ lastUsedAt: 0,
+ createdAt: Date.now(),
+ };
+ await upsertFood.mutateAsync({ food });
+ pickFood(food);
+ };
+
+ // ── база упаковок (Open Food Facts, только штрихкоды) ─────────────────────
+ const offBusy = shallowRef(false);
+ const offError = shallowRef('');
+ const scannerOpen = shallowRef(false);
+ const barcodeInput = shallowRef('');
+ const scanSupported = isBarcodeScanSupported();
+
+ /** Продукт из базы: уже сканировали раньше — сразу к порции, иначе на проверку формы. */
+ const applyOffProduct = (product: OffProduct) => {
+ const existing = foods.value.find(food => food.barcode === product.code);
+ if (existing) {
+ pickFood(existing);
+ return;
+ }
+ foodName.value = product.brand && !product.name.toLowerCase().includes(product.brand.toLowerCase())
+ ? `${product.name} (${product.brand})`
+ : product.name;
+ foodCategory.value = 'Упакованное';
+ foodKcal.value = product.kcal;
+ foodProtein.value = product.protein;
+ foodFat.value = product.fat;
+ foodCarbs.value = product.carbs;
+ foodPiece.value = product.servingGrams ?? 0;
+ foodBarcode.value = product.code;
+ step.value = 'food';
+ };
+
+ const lookupBarcode = async (code: string) => {
+ const digits = code.trim();
+ if (digits === '' || offBusy.value) return;
+ scannerOpen.value = false;
+ offBusy.value = true;
+ offError.value = '';
+ try {
+ const product = await fetchOffByBarcode(digits);
+ if (product) {
+ barcodeInput.value = '';
+ applyOffProduct(product);
+ }
+ else {
+ offError.value = `Штрихкод ${digits} не найден в базе — заведите продукт вручную с упаковки.`;
+ }
+ }
+ catch (cause) {
+ offError.value = cause instanceof Error ? cause.message : 'База недоступна';
+ }
+ finally {
+ offBusy.value = false;
+ }
+ };
+
+ const numeric = (raw: string): number => {
+ const value = Number(raw.replace(',', '.'));
+ return Number.isFinite(value) && value >= 0 ? value : 0;
+ };
+
+ const fieldClass = 'w-full rounded-xl border hairline bg-raised/70 px-3.5 py-2.5 text-[15px] text-ink outline-none transition focus:border-ember/50 placeholder:text-ink-faint';
+ const chipClass = (active: boolean) =>
+ `rounded-full border px-3 py-1.5 text-[13px] transition ${active
+ ? 'border-ember/60 bg-ember/15 text-ember-bright'
+ : 'border-white/10 text-ink-soft hover:border-white/20 hover:text-ink'}`;
+
+ return (
+
+
+
+
+ {/* Шапка */}
+
+ {step.value !== 'pick' && (
+ (step.value = 'pick')}
+ >
+
+
+ )}
+
+ {step.value === 'pick' && `${MEAL_LABELS[meal.value]} · добавить`}
+ {step.value === 'amount' && (selectedFood.value?.name ?? '')}
+ {step.value === 'quick' && 'Быстрая запись'}
+ {step.value === 'food' && 'Новый продукт'}
+
+
+
+
+
+
+ {/* Приём пищи */}
+
+ {MEALS.map(m => (
+ (meal.value = m)}>
+ {MEAL_LABELS[m]}
+
+ ))}
+
+
+
+ {/* ── Шаг: выбор ── */}
+ {step.value === 'pick' && (
+
+
+
+ (query.value = event.currentTarget.value)}
+ class={`${fieldClass} pl-10`}
+ />
+
+
+
+ (step.value = 'quick')}
+ >
+ Только калории
+
+ (step.value = 'food')}
+ >
+ Новый продукт
+
+
+
+ {trimmed.value === '' && recents.value.length > 0 && (
+
+
Недавние
+
+ {recents.value.map(food => (
+ pickFood(food)}
+ >
+ {food.name}
+
+ ))}
+
+
+ )}
+
+ {groups.value.map(([category, list]) => (
+
+
{category}
+
+ {list.map(food => (
+ pickFood(food)}
+ >
+ {food.name}
+
+ {fmtKcal(food.kcal)}
+ {' '}
+ / 100 г
+
+
+ ))}
+
+
+ ))}
+
+ {filtered.value.length === 0 && (
+
+ В каталоге не нашлось — отсканируйте штрихкод упаковки ниже
+ или создайте «Новый продукт».
+
+ )}
+
+ {/* ── База упаковок: только штрихкоды (Open Food Facts) ── */}
+
+
Штрихкод упаковки
+
+ {scanSupported && !scannerOpen.value && (
+
(scannerOpen.value = true)}
+ >
+
+ Сканировать камерой
+
+ )}
+
+ {scannerOpen.value && (
+
void lookupBarcode(code)}
+ onCancel={() => (scannerOpen.value = false)}
+ />
+ )}
+
+
+ (barcodeInput.value = event.currentTarget.value.replaceAll(/\D/g, ''))}
+ onKeydown={(event) => {
+ if (event.key === 'Enter') void lookupBarcode(barcodeInput.value);
+ }}
+ class={`${fieldClass} tnum`}
+ />
+ void lookupBarcode(barcodeInput.value)}
+ >
+ {offBusy.value ? 'Ищем…' : 'Найти'}
+
+
+
+ {offError.value !== '' && (
+
+ {offError.value}
+
+ )}
+
+
+ КБЖУ подтянутся из Open Food Facts — открытой базы упаковок.
+ Её заполняют люди, поэтому цифры стоит сверить с этикеткой.
+
+
+
+ )}
+
+ {/* ── Шаг: порция ── */}
+ {step.value === 'amount' && selectedFood.value && (
+
+
+
{preview.value ? fmtKcal(preview.value.kcal) : 0}
+
ккал в порции
+
+
+ Б
+ {' '}
+ {fmtG(preview.value?.protein ?? 0)}
+
+
+ Ж
+ {' '}
+ {fmtG(preview.value?.fat ?? 0)}
+
+
+ У
+ {' '}
+ {fmtG(preview.value?.carbs ?? 0)}
+
+
+
+
+
+
Порция, граммы
+
(amountG.value = numeric(event.currentTarget.value))}
+ class={`${fieldClass} text-center text-lg tnum`}
+ />
+
+ {GRAM_PRESETS.map(grams => (
+ (amountG.value = grams)}>
+ {grams}
+ {' '}
+ г
+
+ ))}
+
+ {selectedFood.value.pieceGrams && (
+
+ {[1, 2, 3].map(count => (
+ (amountG.value = count * (selectedFood.value?.pieceGrams ?? 0))}
+ >
+ {`${count} шт · ${count * (selectedFood.value?.pieceGrams ?? 0)} г`}
+
+ ))}
+
+ )}
+
+
+
+ Добавить в
+ {' '}
+ {MEAL_LABELS[meal.value].toLowerCase()}
+
+
+ )}
+
+ {/* ── Шаг: быстрая запись ── */}
+ {step.value === 'quick' && (
+
+
+ Когда некогда взвешивать — запишите оценку калорий, чтобы день остался честным.
+ Б/Ж/У можно не указывать.
+
+
(quickName.value = event.currentTarget.value)}
+ class={fieldClass}
+ />
+
+ Калории
+ (quickKcal.value = numeric(event.currentTarget.value))}
+ class={`${fieldClass} text-center text-lg tnum`}
+ />
+
+
+ {([
+ ['Белки', quickProtein],
+ ['Жиры', quickFat],
+ ['Углеводы', quickCarbs],
+ ] as const).map(([label, model]) => (
+
+
+ {label}
+ , г
+
+ (model.value = numeric(event.currentTarget.value))}
+ class={`${fieldClass} text-center tnum`}
+ />
+
+ ))}
+
+
+ Записать
+
+
+ )}
+
+ {/* ── Шаг: новый продукт ── */}
+ {step.value === 'food' && (
+
+
(foodName.value = event.currentTarget.value)}
+ class={fieldClass}
+ />
+
+ Категория
+ (foodCategory.value = event.currentTarget.value)}
+ class={fieldClass}
+ />
+
+ {categories.value.map(category => (
+
+ ))}
+
+
+ {foodBarcode.value !== ''
+ ? (
+
+ Значения подставлены из Open Food Facts — сверьте с этикеткой и поправьте, если расходятся.
+
+ )
+ :
Значения указываются на 100 г продукта.
}
+
+
+ {([
+ ['Белки', foodProtein],
+ ['Жиры', foodFat],
+ ['Углеводы', foodCarbs],
+ ] as const).map(([label, model]) => (
+
+
+ {label}
+ , г
+
+ (model.value = numeric(event.currentTarget.value))}
+ class={`${fieldClass} text-center tnum`}
+ />
+
+ ))}
+
+
+ Сохранить и выбрать порцию
+
+
+ )}
+
+
+
+ );
+}
diff --git a/kcal/src/screens/DiaryScreen.tsx b/kcal/src/screens/DiaryScreen.tsx
new file mode 100644
index 0000000..c23459b
--- /dev/null
+++ b/kcal/src/screens/DiaryScreen.tsx
@@ -0,0 +1,133 @@
+import { computed } from 'vue';
+import { useEntity, useQuery } from 'vue-sync-engine';
+import { EntryEntity, FoodEntity, ProfileEntity, entriesByDayQuery, foodsQuery } from '../data/defs';
+import { useEntities } from '../data/composables';
+import { sumNutrients } from '../domain/calc';
+import { dayTitle, shiftISODate, todayISO } from '../domain/dates';
+import { fmtAmount, fmtKcal } from '../domain/format';
+import { MEALS, MEAL_LABELS, PROFILE_ID } from '../domain/types';
+import type { Entry, Meal } from '../domain/types';
+import { editEntryId, goToday, openAddSheet, selectedDate } from '../ui/state';
+import { IconChevronLeft, IconChevronRight, IconPlus } from '../ui/icons';
+import ProgressRing from '../components/ProgressRing';
+import MacroBar from '../components/MacroBar';
+
+export default function DiaryScreen() {
+ const profile = useEntity(ProfileEntity, () => PROFILE_ID);
+ const day = useQuery(entriesByDayQuery, () => ({ date: selectedDate.value }));
+ const entries = useEntities(EntryEntity, () => day.data.value?.ids);
+ // Каталог нужен только ради pieceGrams — чтобы подписывать порции «2 шт · 110 г».
+ const foodsQ = useQuery(foodsQuery, () => undefined);
+ const foods = useEntities(FoodEntity, () => foodsQ.data.value?.ids);
+ const pieceByFoodId = computed(() => {
+ const map = new Map();
+ for (const food of foods.value) map.set(food.id, food.pieceGrams);
+ return map;
+ });
+ const totals = computed(() => sumNutrients(entries.value));
+ const isToday = computed(() => selectedDate.value === todayISO());
+
+ const byMeal = computed(() => {
+ const map = new Map();
+ for (const meal of MEALS) map.set(meal, []);
+ for (const entry of entries.value) map.get(entry.meal)?.push(entry);
+ return map;
+ });
+
+ return (
+
+ {/* Навигация по дням */}
+
+
(selectedDate.value = shiftISODate(selectedDate.value, -1))}
+ >
+
+
+
+
{dayTitle(selectedDate.value)}
+ {!isToday.value && (
+
+ вернуться к сегодня
+
+ )}
+
+
(selectedDate.value = shiftISODate(selectedDate.value, 1))}
+ >
+
+
+
+
+ {/* Итог дня: кольцо + макросы */}
+
+
+ {/* Приёмы пищи */}
+ {MEALS.map((meal, index) => {
+ const list = byMeal.value.get(meal) ?? [];
+ const mealKcal = list.reduce((acc, entry) => acc + entry.kcal, 0);
+ return (
+
+
+
{MEAL_LABELS[meal]}
+ {list.length > 0 && (
+
+ {fmtKcal(mealKcal)}
+ {' '}
+ ккал
+
+ )}
+
+
+ {list.map(entry => (
+
(editEntryId.value = entry.id)}
+ >
+
+
{entry.name}
+
+ {fmtAmount(entry.amountG, entry.foodId ? pieceByFoodId.value.get(entry.foodId) : undefined)}
+
+
+ {fmtKcal(entry.kcal)}
+
+ ))}
+
openAddSheet(meal)}
+ >
+
+ Добавить
+
+
+
+ );
+ })}
+
+ {entries.value.length === 0 && (
+
+ Пока пусто. Нажмите «Добавить» в любом приёме пищи —
+ недавние продукты будут под рукой, запись занимает пару касаний.
+
+ )}
+
+ );
+}
diff --git a/kcal/src/screens/EditEntrySheet.tsx b/kcal/src/screens/EditEntrySheet.tsx
new file mode 100644
index 0000000..183854b
--- /dev/null
+++ b/kcal/src/screens/EditEntrySheet.tsx
@@ -0,0 +1,181 @@
+import { computed, shallowRef } from 'vue';
+import { useEntity, useMutation } from 'vue-sync-engine';
+import { useCloseWatcher } from '@robonen/vue';
+import { EntryEntity, removeEntryMutation, updateEntryMutation } from '../data/defs';
+import { round1 } from '../domain/calc';
+import { fmtG, fmtKcal } from '../domain/format';
+import { MEALS, MEAL_LABELS } from '../domain/types';
+import type { Entry, Meal } from '../domain/types';
+import { editEntryId } from '../ui/state';
+import { IconClose, IconTrash } from '../ui/icons';
+
+/**
+ * Правка записи: порция (пересчёт нутриентов пропорцией — работает и для
+ * записей без продукта-источника), приём пищи, удаление.
+ */
+export default function EditEntrySheet() {
+ const entry = useEntity(EntryEntity, () => editEntryId.value ?? undefined);
+
+ const update = useMutation(updateEntryMutation);
+ const remove = useMutation(removeEntryMutation);
+
+ const close = () => (editEntryId.value = null);
+ useCloseWatcher().onClose(close);
+
+ // Локальные правки поверх записи; null — «не менялось».
+ const draftAmount = shallowRef(null);
+ const draftKcal = shallowRef(null);
+ const draftMeal = shallowRef(null);
+
+ const amount = computed(() => draftAmount.value ?? entry.value?.amountG ?? 0);
+ const meal = computed(() => draftMeal.value ?? entry.value?.meal ?? 'snack');
+
+ /** Нутриенты после правки порции: масштабируем снапшот записи. */
+ const scaled = computed(() => {
+ const current = entry.value;
+ if (!current) return null;
+ if (current.amountG && draftAmount.value !== null && draftAmount.value > 0) {
+ const factor = draftAmount.value / current.amountG;
+ return {
+ kcal: Math.round(current.kcal * factor),
+ protein: round1(current.protein * factor),
+ fat: round1(current.fat * factor),
+ carbs: round1(current.carbs * factor),
+ };
+ }
+ if (!current.amountG && draftKcal.value !== null) {
+ return { kcal: Math.round(draftKcal.value), protein: current.protein, fat: current.fat, carbs: current.carbs };
+ }
+ return { kcal: current.kcal, protein: current.protein, fat: current.fat, carbs: current.carbs };
+ });
+
+ const save = () => {
+ const current = entry.value;
+ const nutrients = scaled.value;
+ if (!current || !nutrients) return;
+ const patch: Partial = { ...nutrients, meal: meal.value };
+ if (current.amountG && draftAmount.value !== null && draftAmount.value > 0) {
+ patch.amountG = draftAmount.value;
+ }
+ update.mutate({ id: current.id, patch });
+ close();
+ };
+
+ const removeEntry = () => {
+ const current = entry.value;
+ if (!current) return;
+ remove.mutate({ id: current.id });
+ close();
+ };
+
+ const numeric = (raw: string): number => {
+ const value = Number(raw.replace(',', '.'));
+ return Number.isFinite(value) && value >= 0 ? value : 0;
+ };
+
+ const fieldClass = 'w-full rounded-xl border hairline bg-raised/70 px-3.5 py-2.5 text-[15px] text-ink outline-none transition focus:border-ember/50';
+ const chipClass = (active: boolean) =>
+ `rounded-full border px-3 py-1.5 text-[13px] transition ${active
+ ? 'border-ember/60 bg-ember/15 text-ember-bright'
+ : 'border-white/10 text-ink-soft hover:border-white/20 hover:text-ink'}`;
+
+ return (
+
+
+
+
+
+
{entry.value?.name ?? ''}
+
+
+
+
+
+ {entry.value && (
+
+
+
{fmtKcal(scaled.value?.kcal ?? 0)}
+
ккал
+
+
+ Б
+ {' '}
+ {fmtG(scaled.value?.protein ?? 0)}
+
+
+ Ж
+ {' '}
+ {fmtG(scaled.value?.fat ?? 0)}
+
+
+ У
+ {' '}
+ {fmtG(scaled.value?.carbs ?? 0)}
+
+
+
+
+
+ {MEALS.map(m => (
+ (draftMeal.value = m)}>
+ {MEAL_LABELS[m]}
+
+ ))}
+
+
+ {entry.value.amountG
+ ? (
+
+ Порция, граммы
+ (draftAmount.value = numeric(event.currentTarget.value))}
+ class={`${fieldClass} text-center text-lg tnum`}
+ />
+
+ )
+ : (
+
+ Калории
+ (draftKcal.value = numeric(event.currentTarget.value))}
+ class={`${fieldClass} text-center text-lg tnum`}
+ />
+
+ )}
+
+
+
+
+
+
+ Сохранить
+
+
+
+ )}
+
+
+ );
+}
diff --git a/kcal/src/screens/FoodFormSheet.tsx b/kcal/src/screens/FoodFormSheet.tsx
new file mode 100644
index 0000000..7c7a2a8
--- /dev/null
+++ b/kcal/src/screens/FoodFormSheet.tsx
@@ -0,0 +1,188 @@
+import { computed, shallowRef } from 'vue';
+import { useEntity, useMutation, useQuery } from 'vue-sync-engine';
+import { useCloseWatcher } from '@robonen/vue';
+import { FoodEntity, foodsQuery, removeFoodMutation, upsertFoodMutation } from '../data/defs';
+import { useEntities } from '../data/composables';
+import type { Food } from '../domain/types';
+import { foodForm } from '../ui/state';
+import { IconClose, IconTrash } from '../ui/icons';
+
+/** Создание и правка продукта каталога. Значения нутриентов — на 100 г. */
+export default function FoodFormSheet() {
+ const existing = useEntity(FoodEntity, () => foodForm.foodId ?? undefined);
+ const foodsQ = useQuery(foodsQuery, () => undefined);
+ const foods = useEntities(FoodEntity, () => foodsQ.data.value?.ids);
+
+ const upsert = useMutation(upsertFoodMutation);
+ const remove = useMutation(removeFoodMutation);
+
+ const close = () => (foodForm.open = false);
+ useCloseWatcher().onClose(close);
+
+ const source = existing.value;
+ const name = shallowRef(source?.name ?? '');
+ const category = shallowRef(source?.category ?? 'Моё');
+ const kcal = shallowRef(source?.kcal ?? 0);
+ const protein = shallowRef(source?.protein ?? 0);
+ const fat = shallowRef(source?.fat ?? 0);
+ const carbs = shallowRef(source?.carbs ?? 0);
+ const pieceGrams = shallowRef(source?.pieceGrams ?? 0);
+
+ const categories = computed(() => [...new Set(foods.value.map(food => food.category))].sort((a, b) => a.localeCompare(b, 'ru')));
+
+ const save = () => {
+ if (name.value.trim() === '' || kcal.value <= 0) return;
+ const base = existing.value;
+ const food: Food = {
+ id: base?.id ?? crypto.randomUUID(),
+ name: name.value.trim(),
+ category: category.value.trim() || 'Моё',
+ kcal: kcal.value,
+ protein: protein.value,
+ fat: fat.value,
+ carbs: carbs.value,
+ ...(pieceGrams.value > 0 ? { pieceGrams: pieceGrams.value } : {}),
+ usedCount: base?.usedCount ?? 0,
+ lastUsedAt: base?.lastUsedAt ?? 0,
+ ...(base?.lastAmountG ? { lastAmountG: base.lastAmountG } : {}),
+ createdAt: base?.createdAt ?? Date.now(),
+ };
+ upsert.mutate({ food });
+ close();
+ };
+
+ const removeFood = () => {
+ const base = existing.value;
+ if (!base) return;
+ remove.mutate({ id: base.id });
+ close();
+ };
+
+ const numeric = (raw: string): number => {
+ const value = Number(raw.replace(',', '.'));
+ return Number.isFinite(value) && value >= 0 ? value : 0;
+ };
+
+ const fieldClass = 'w-full rounded-xl border hairline bg-raised/70 px-3.5 py-2.5 text-[15px] text-ink outline-none transition focus:border-ember/50 placeholder:text-ink-faint';
+
+ return (
+
+
+
+
+
+
{existing.value ? 'Продукт' : 'Новый продукт'}
+
+
+
+
+
+
+
(name.value = event.currentTarget.value)}
+ class={fieldClass}
+ />
+
+ Категория
+ (category.value = event.currentTarget.value)}
+ class={fieldClass}
+ />
+
+ {categories.value.map(item => (
+
+ ))}
+
+
+
+
Значения указываются на 100 г продукта.
+
+
+ {([
+ ['Белки', protein],
+ ['Жиры', fat],
+ ['Углеводы', carbs],
+ ] as const).map(([label, model]) => (
+
+
+ {label}
+ , г
+
+ (model.value = numeric(event.currentTarget.value))}
+ class={`${fieldClass} text-center tnum`}
+ />
+
+ ))}
+
+
+
+ {existing.value && (
+
+
+
+ )}
+
+ Сохранить
+
+
+
+ {existing.value && (
+
+ Записи в дневнике хранят свой снимок значений — правка продукта
+ не меняет уже записанные дни.
+
+ )}
+
+
+
+ );
+}
diff --git a/kcal/src/screens/FoodsScreen.tsx b/kcal/src/screens/FoodsScreen.tsx
new file mode 100644
index 0000000..d2ca16e
--- /dev/null
+++ b/kcal/src/screens/FoodsScreen.tsx
@@ -0,0 +1,99 @@
+import { computed, shallowRef } from 'vue';
+import { useQuery } from 'vue-sync-engine';
+import { groupBy } from '@robonen/stdlib';
+import { FoodEntity, foodsQuery } from '../data/defs';
+import { useEntities } from '../data/composables';
+import { fmtG, fmtKcal } from '../domain/format';
+import { foodForm } from '../ui/state';
+import { IconPlus, IconSearch } from '../ui/icons';
+
+export default function FoodsScreen() {
+ const foodsQ = useQuery(foodsQuery, () => undefined);
+ const foods = useEntities(FoodEntity, () => foodsQ.data.value?.ids);
+ const query = shallowRef('');
+
+ const filtered = computed(() => {
+ const needle = query.value.trim().toLowerCase();
+ return needle === '' ? foods.value : foods.value.filter(food => food.name.toLowerCase().includes(needle));
+ });
+
+ const groups = computed(() => {
+ const grouped = groupBy(filtered.value, food => food.category);
+ return Object.entries(grouped).sort((a, b) => a[0].localeCompare(b[0], 'ru'));
+ });
+
+ return (
+
+
+
Продукты
+ {
+ foodForm.foodId = null;
+ foodForm.open = true;
+ }}
+ >
+
+ Новый
+
+
+
+
+
+ (query.value = event.currentTarget.value)}
+ class="w-full rounded-xl border hairline bg-raised/70 py-2.5 pr-3.5 pl-10 text-[15px] text-ink outline-none transition focus:border-ember/50 placeholder:text-ink-faint"
+ />
+
+
+ {groups.value.map(([category, list], index) => (
+
+ {category}
+
+ {list.map(food => (
+
{
+ foodForm.foodId = food.id;
+ foodForm.open = true;
+ }}
+ >
+
+
{food.name}
+
+ Б
+ {' '}
+ {fmtG(food.protein)}
+ {' '}
+ · Ж
+ {' '}
+ {fmtG(food.fat)}
+ {' '}
+ · У
+ {' '}
+ {fmtG(food.carbs)}
+ {food.pieceGrams ? ` · 1 шт = ${fmtG(food.pieceGrams)} г` : ''}
+
+
+
+ {fmtKcal(food.kcal)}
+ {' '}
+ /100 г
+
+
+ ))}
+
+
+ ))}
+
+ {filtered.value.length === 0 && (
+
Ничего не нашлось.
+ )}
+
+ );
+}
diff --git a/kcal/src/screens/HelpSection.tsx b/kcal/src/screens/HelpSection.tsx
new file mode 100644
index 0000000..94c219b
--- /dev/null
+++ b/kcal/src/screens/HelpSection.tsx
@@ -0,0 +1,88 @@
+import { IconChevronRight } from '../ui/icons';
+
+interface FaqItem {
+ q: string;
+ a: string[];
+}
+
+const FAQ: FaqItem[] = [
+ {
+ q: 'Откуда брать калории и БЖУ?',
+ a: [
+ 'Упакованные продукты: КБЖУ на 100 г напечатаны на этикетке — заведите «Новый продукт» и перепишите 4 числа, либо отсканируйте штрихкод, и значения подставятся из открытой базы. Делается один раз: дальше продукт живёт в каталоге и всплывает в «Недавних».',
+ 'Обычная еда без этикетки (крупы, мясо, овощи): большая часть уже в каталоге, гарниры — в готовом виде, как на весах. Если чего-то нет — поищите «название калорийность на 100 г»: Calorizator, health-diet.ru (таблицы Скурихина), FatSecret дают рабочие цифры.',
+ 'Со временем ввод почти исчезает: вы едите одни и те же 20–30 продуктов, и все они окажутся в каталоге за первые пару недель.',
+ ],
+ },
+ {
+ q: 'Как записывать ресторан и доставку?',
+ a: [
+ 'Точных цифр тут не знает никто — используйте «Только калории» и записывайте оценку. День с примерной записью полезнее дня без записи.',
+ 'Ориентиры: тарелка бизнес-ланча 600–800 ккал, бургер с картошкой ~1000, кусок пиццы 250–300, салат с заправкой 150–250, сладкий кофе 150–300.',
+ 'У сетей (Вкусно и точка, KFC, Додо) КБЖУ опубликованы на сайте или в приложении — можно завести как продукт.',
+ ],
+ },
+ {
+ q: 'Насколько точно нужно считать?',
+ a: [
+ 'Не точно — консистентно. Ошибка ±20% на глаз — это нормально: она систематическая, то есть постоянная, и её выравнивает обратная связь по весу.',
+ 'Раз в день (утром, натощак) записывайте вес и смотрите на тренд за 1–2 недели, а не на суточные скачки. Записываете ~2000 ккал, а вес стоит при цели «похудение»? Значит, реальные калории выше — просто уменьшите цель на 200–300.',
+ 'Главная причина бросить дневник — перфекционизм. Лучше записать примерно, чем не записать вовсе.',
+ ],
+ },
+ {
+ q: 'Как считать домашние блюда?',
+ a: [
+ 'Пока — приближением: в каталоге есть типовые готовые блюда (борщ, плов, сырники), их значения усреднённые, но стабильные.',
+ 'Хотите точнее — заведите свой продукт: сложите КБЖУ сырых ингредиентов всей кастрюли, разделите на общий готовый вес и получите значения «на 100 г» своего блюда.',
+ 'Конструктор рецептов, который посчитает это сам, — в планах.',
+ ],
+ },
+ {
+ q: 'Как считаются мои цели?',
+ a: [
+ 'База — формула Миффлина—Сан Жеора: базовый обмен из пола, возраста, роста и веса, умноженный на коэффициент активности, даёт суточный расход.',
+ 'Похудение — умеренный дефицит 15% (комфортный темп ~0,5 кг в неделю), набор — профицит 10%. Белок 1,6–1,8 г на кг веса — для сытости и сохранения мышц; жиры ~1 г/кг; углеводы — остаток калорий.',
+ 'Это стартовая точка, а не приговор: цели можно править вручную в профиле, и стоит корректировать их по фактическому тренду веса.',
+ ],
+ },
+ {
+ q: 'Как работает сканер штрихкодов?',
+ a: [
+ 'Сканер и поле «Цифры под штрихкодом» ищут упаковку в Open Food Facts — открытой базе продуктов, которую заполняют люди. Запрос уходит напрямую из браузера, свой сервер для этого не нужен.',
+ 'Качество данных разное: перед сохранением приложение показывает форму с подставленными значениями — сверьте их с этикеткой. Повторное сканирование того же товара сразу открывает выбор порции.',
+ 'Камера работает в Chrome и на Android (нужно разрешение); если сканера нет — введите цифры, напечатанные под полосками штрихкода.',
+ ],
+ },
+ {
+ q: 'Где хранятся мои данные?',
+ a: [
+ 'Только на этом устройстве, в IndexedDB браузера. Никаких аккаунтов, серверов и синхронизации — данные не покидают браузер.',
+ 'Обратная сторона: браузер может удалить данные сайта (очистка хранилища, «стереть данные посещённых сайтов»). Поэтому время от времени делайте «Экспорт JSON» в разделе «Данные» — файл восстановит всё через «Импорт».',
+ ],
+ },
+];
+
+/** Справка: практические ответы для первых недель ведения дневника. */
+export default function HelpSection() {
+ return (
+
+ Справка
+
+ {FAQ.map(item => (
+
+
+
+ {item.q}
+
+
+ {item.a.map(paragraph => (
+
{paragraph}
+ ))}
+
+
+ ))}
+
+
+ );
+}
diff --git a/kcal/src/screens/ProfileScreen.tsx b/kcal/src/screens/ProfileScreen.tsx
new file mode 100644
index 0000000..d574e29
--- /dev/null
+++ b/kcal/src/screens/ProfileScreen.tsx
@@ -0,0 +1,351 @@
+import { computed, shallowRef, watch } from 'vue';
+import { Status, useEntity, useMutation } from 'vue-sync-engine';
+import { DB_NAME, ProfileEntity, saveProfileMutation } from '../data/defs';
+import { downloadBackupFile, exportBackup, importBackup } from '../data/backup';
+import type { BackupPayload } from '../data/backup';
+import { bmr, computeTargets, roundTo, safeKcalFloor, tdee } from '../domain/calc';
+import { fmtKcal } from '../domain/format';
+import { ACTIVITY_LEVELS, GOAL_LABELS, PROFILE_ID } from '../domain/types';
+import type { Goal, Profile, Sex } from '../domain/types';
+import { IconDownload, IconUpload } from '../ui/icons';
+import HelpSection from './HelpSection';
+
+const GOALS: readonly Goal[] = ['lose', 'maintain', 'gain'];
+
+/** Онбординг при первом входе и настройки в дальнейшем — одна форма. */
+export default function ProfileScreen(props: { onboarding?: boolean }) {
+ const profile = useEntity(ProfileEntity, () => PROFILE_ID);
+ const save = useMutation(saveProfileMutation);
+
+ const source = profile.value;
+ const sex = shallowRef(source?.sex ?? 'male');
+ const age = shallowRef(source?.age ?? 30);
+ const heightCm = shallowRef(source?.heightCm ?? 175);
+ const weightKg = shallowRef(source?.weightKg ?? 75);
+ const activity = shallowRef(source?.activity ?? 1.375);
+ const goal = shallowRef(source?.goal ?? 'lose');
+
+ const targetKcal = shallowRef(source?.targetKcal ?? 0);
+ const targetProtein = shallowRef(source?.targetProtein ?? 0);
+ const targetFat = shallowRef(source?.targetFat ?? 0);
+ const targetCarbs = shallowRef(source?.targetCarbs ?? 0);
+
+ const valid = computed(() =>
+ age.value >= 10 && age.value <= 100
+ && heightCm.value >= 120 && heightCm.value <= 230
+ && weightKg.value >= 30 && weightKg.value <= 300,
+ );
+
+ const recompute = () => {
+ if (!valid.value) return;
+ const targets = computeTargets({
+ sex: sex.value,
+ age: age.value,
+ heightCm: heightCm.value,
+ weightKg: weightKg.value,
+ activity: activity.value,
+ goal: goal.value,
+ });
+ targetKcal.value = targets.kcal;
+ targetProtein.value = targets.protein;
+ targetFat.value = targets.fat;
+ targetCarbs.value = targets.carbs;
+ };
+
+ // Цели следуют за параметрами; ручную правку ниже можно сделать перед сохранением.
+ watch([sex, age, heightCm, weightKg, activity, goal], recompute);
+ if (!source) recompute();
+
+ const bmrValue = computed(() =>
+ valid.value ? Math.round(bmr(sex.value, age.value, heightCm.value, weightKg.value)) : 0,
+ );
+ const tdeeValue = computed(() =>
+ valid.value ? roundTo(tdee(sex.value, age.value, heightCm.value, weightKg.value, activity.value), 10) : 0,
+ );
+ const belowFloor = computed(() => targetKcal.value > 0 && targetKcal.value < safeKcalFloor(sex.value));
+
+ const submit = () => {
+ if (!valid.value || targetKcal.value <= 0) return;
+ const now = Date.now();
+ const next: Profile = {
+ id: PROFILE_ID,
+ sex: sex.value,
+ age: age.value,
+ heightCm: heightCm.value,
+ weightKg: weightKg.value,
+ activity: activity.value,
+ goal: goal.value,
+ targetKcal: targetKcal.value,
+ targetProtein: targetProtein.value,
+ targetFat: targetFat.value,
+ targetCarbs: targetCarbs.value,
+ createdAt: profile.value?.createdAt ?? now,
+ updatedAt: now,
+ };
+ save.mutate({ profile: next });
+ };
+
+ // ── бэкап ─────────────────────────────────────────────────────────────────
+ const importing = shallowRef(false);
+ const notice = shallowRef('');
+
+ const doExport = async () => {
+ downloadBackupFile(await exportBackup());
+ };
+
+ const doImport = async (file: File | undefined) => {
+ if (!file) return;
+ importing.value = true;
+ try {
+ const payload = JSON.parse(await file.text()) as BackupPayload;
+ await importBackup(payload);
+ location.reload();
+ }
+ catch (error) {
+ notice.value = error instanceof Error ? error.message : 'Не удалось прочитать файл';
+ importing.value = false;
+ }
+ };
+
+ const wipeAll = () => {
+ if (!globalThis.confirm('Удалить все данные приложения? Действие необратимо.')) return;
+ indexedDB.deleteDatabase(DB_NAME);
+ location.reload();
+ };
+
+ const numeric = (raw: string): number => {
+ const value = Number(raw.replace(',', '.'));
+ return Number.isFinite(value) && value >= 0 ? value : 0;
+ };
+
+ const fieldClass = 'w-full rounded-xl border hairline bg-raised/70 px-3.5 py-2.5 text-[15px] text-ink outline-none transition focus:border-ember/50 tnum';
+ const chipClass = (active: boolean) =>
+ `flex-1 rounded-xl border px-3 py-2 text-[13px] transition ${active
+ ? 'border-ember/60 bg-ember/15 text-ember-bright'
+ : 'border-white/10 text-ink-soft hover:border-white/20 hover:text-ink'}`;
+
+ return (
+
+
+ {props.onboarding
+ ? (
+
+
Ккал
+
Настроим дневник
+
+ Пара параметров — и посчитаем вашу дневную норму калорий и белка.
+ Всё хранится только на этом устройстве.
+
+
+ )
+ :
Профиль }
+
+
+ {/* Параметры тела */}
+
+
+ (sex.value = 'male')}>Мужчина
+ (sex.value = 'female')}>Женщина
+
+
+
+
+
+
Активность
+
+ {ACTIVITY_LEVELS.map(level => (
+ (activity.value = level.value)}
+ >
+ {level.label}
+ {level.hint}
+
+ ))}
+
+
+
+
+
Цель
+
+ {GOALS.map(value => (
+ (goal.value = value)}>
+ {GOAL_LABELS[value]}
+
+ ))}
+
+
+
+
+ {/* Расчёт и цели */}
+
+
+
+
{fmtKcal(bmrValue.value)}
+
базовый обмен
+
+
+
{fmtKcal(tdeeValue.value)}
+
суточный расход
+
+
+
+
+
+ {belowFloor.value && (
+
+ Цель ниже безопасного минимума (
+ {fmtKcal(safeKcalFloor(sex.value))}
+ {' '}
+ ккал).
+ Долгий жёсткий дефицит вредит — лучше худеть медленнее.
+
+ )}
+
+
+ Цели пересчитываются из параметров выше, но перед сохранением их можно
+ поправить вручную. Формула — Миффлина—Сан Жеора.
+
+
+
+ {props.onboarding ? 'Начать вести дневник' : 'Сохранить'}
+
+ {!props.onboarding && save.status.value === Status.Success && (
+ Сохранено
+ )}
+
+
+ {/* Данные */}
+ {!props.onboarding && (
+
+ Данные
+
+
+
+ Экспорт JSON
+
+
+
+ {importing.value ? 'Импорт…' : 'Импорт JSON'}
+ void doImport(event.currentTarget.files?.[0])}
+ />
+
+
+ {notice.value && {notice.value}
}
+
+ Всё хранится в IndexedDB этого браузера. Делайте экспорт время от
+ времени — браузер может очистить данные сайта.
+
+
+ Стереть все данные
+
+
+ )}
+
+ {!props.onboarding &&
}
+
+ );
+}
diff --git a/kcal/src/screens/StatsScreen.tsx b/kcal/src/screens/StatsScreen.tsx
new file mode 100644
index 0000000..b61ac8f
--- /dev/null
+++ b/kcal/src/screens/StatsScreen.tsx
@@ -0,0 +1,303 @@
+import { computed, shallowRef } from 'vue';
+import { useEntity, useMutation, useQuery } from 'vue-sync-engine';
+import {
+ ProfileEntity,
+ WeightEntity,
+ daySummariesQuery,
+ logWeightMutation,
+ removeWeightMutation,
+ weightsQuery,
+} from '../data/defs';
+import type { DaySummary } from '../data/defs';
+import { useEntities } from '../data/composables';
+import { dayShort, dayTitle, lastDays, todayISO } from '../domain/dates';
+import { fmtG, fmtKcal } from '../domain/format';
+import { PROFILE_ID } from '../domain/types';
+import type { WeightLog } from '../domain/types';
+import { activeTab, selectedDate } from '../ui/state';
+import { IconScale, IconTrash } from '../ui/icons';
+
+const PERIODS = [7, 14, 30] as const;
+
+export default function StatsScreen() {
+ const profile = useEntity(ProfileEntity, () => PROFILE_ID);
+ const summariesQ = useQuery(daySummariesQuery, () => undefined);
+ const weightsQ = useQuery(weightsQuery, () => undefined);
+ const weights = useEntities(WeightEntity, () => weightsQ.data.value?.ids);
+
+ const logWeight = useMutation(logWeightMutation);
+ const removeWeight = useMutation(removeWeightMutation);
+
+ const period = shallowRef<7 | 14 | 30>(14);
+ const selectedBar = shallowRef(todayISO());
+
+ const target = computed(() => profile.value?.targetKcal ?? 2000);
+
+ const days = computed(() => {
+ const byDate = new Map((summariesQ.data.value ?? []).map(day => [day.date, day]));
+ return lastDays(period.value).map(date =>
+ byDate.get(date) ?? { date, kcal: 0, protein: 0, fat: 0, carbs: 0, entries: 0 },
+ );
+ });
+
+ const chartMax = computed(() => {
+ const peak = Math.max(target.value, ...days.value.map(day => day.kcal));
+ return peak * 1.08;
+ });
+
+ const selectedDay = computed(() =>
+ days.value.find(day => day.date === selectedBar.value) ?? null,
+ );
+
+ const tracked = computed(() => days.value.filter(day => day.entries > 0));
+ const averages = computed(() => {
+ const list = tracked.value;
+ if (list.length === 0) return null;
+ const total = list.reduce(
+ (acc, day) => ({ kcal: acc.kcal + day.kcal, protein: acc.protein + day.protein }),
+ { kcal: 0, protein: 0 },
+ );
+ const onTarget = list.filter(day => day.kcal <= target.value).length;
+ return {
+ kcal: total.kcal / list.length,
+ protein: total.protein / list.length,
+ onTargetShare: Math.round((onTarget / list.length) * 100),
+ };
+ });
+
+ // ── вес ───────────────────────────────────────────────────────────────────
+ const weightInput = shallowRef(0);
+ const latestWeight = computed(() => weights.value.at(-1) ?? null);
+ const weightDelta = computed(() => {
+ const list = weights.value;
+ const latest = list.at(-1);
+ if (!latest || list.length < 2) return null;
+ const weekAgoDate = lastDays(8)[0] ?? latest.date;
+ const reference = [...list].reverse().find(item => item.date <= weekAgoDate) ?? list[0];
+ if (!reference || reference.id === latest.id) return null;
+ return latest.kg - reference.kg;
+ });
+
+ const sparkPoints = computed(() => {
+ const list = weights.value.slice(-30);
+ if (list.length < 2) return '';
+ const min = Math.min(...list.map(item => item.kg));
+ const max = Math.max(...list.map(item => item.kg));
+ const span = Math.max(max - min, 0.5);
+ return list
+ .map((item, index) => {
+ const x = (index / (list.length - 1)) * 100;
+ const y = 26 - ((item.kg - min) / span) * 22 + 2;
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
+ })
+ .join(' ');
+ });
+
+ const submitWeight = () => {
+ if (weightInput.value <= 0) return;
+ const date = todayISO();
+ const weight: WeightLog = { id: date, date, kg: weightInput.value, createdAt: Date.now() };
+ logWeight.mutate({ weight });
+ weightInput.value = 0;
+ };
+
+ const openInDiary = (day: DaySummary) => {
+ selectedDate.value = day.date;
+ activeTab.value = 'diary';
+ };
+
+ const numeric = (raw: string): number => {
+ const value = Number(raw.replace(',', '.'));
+ return Number.isFinite(value) && value >= 0 ? value : 0;
+ };
+
+ return (
+
+
+
Статистика
+
+ {PERIODS.map(value => (
+ (period.value = value)}
+ >
+ {value}
+ {' '}
+ дн
+
+ ))}
+
+
+
+ {/* График калорий по дням */}
+
+
+
Калории по дням
+
+ {`цель ${fmtKcal(target.value)}`}
+
+
+
+ {selectedDay.value && (
+ selectedDay.value && openInDiary(selectedDay.value)}
+ >
+ {dayTitle(selectedDay.value.date)}
+
+ {selectedDay.value.entries > 0
+ ? (
+
+ {fmtKcal(selectedDay.value.kcal)}
+ {' '}
+ ккал · Б
+ {' '}
+ {fmtG(selectedDay.value.protein)}
+
+ )
+ : нет записей }
+
+
+ )}
+
+
+ {/* Линия цели */}
+
+
+ {days.value.map(day => (
+
(selectedBar.value = day.date)}
+ >
+ 0 ? '' : 'min-h-[2px]'} ${selectedBar.value === day.date
+ ? 'bg-ember-bright'
+ : day.kcal > target.value ? 'bg-over/80 group-hover:bg-over' : 'bg-ember/65 group-hover:bg-ember'}`}
+ style={{ height: `${Math.max((day.kcal / chartMax.value) * 100, day.kcal > 0 ? 2 : 1)}%` }}
+ />
+
+ ))}
+
+
+
+ {dayShort(days.value[0]?.date ?? todayISO())}
+ {dayShort(days.value.at(-1)?.date ?? todayISO())}
+
+
+
+ {/* Средние за период */}
+ {averages.value && (
+
+
+
{fmtKcal(averages.value.kcal)}
+
ккал в среднем
+
+
+
{fmtG(averages.value.protein)}
+
белка в день, г
+
+
+
+ {averages.value.onTargetShare}
+ %
+
+
дней в цели
+
+
+ )}
+
+ {/* Вес */}
+
+
+
+
Вес
+ {weightDelta.value !== null && (
+
+ {weightDelta.value > 0 ? '+' : ''}
+ {fmtG(weightDelta.value)}
+ {' '}
+ кг за неделю
+
+ )}
+
+
+ {latestWeight.value && (
+
+
+ {fmtG(latestWeight.value.kg)}
+ кг
+
+ {sparkPoints.value && (
+
+
+
+ )}
+
+ )}
+
+
+ (weightInput.value = numeric(event.currentTarget.value))}
+ class="w-full flex-1 rounded-xl border hairline bg-raised/70 px-3.5 py-2.5 text-[15px] text-ink outline-none transition focus:border-ember/50 placeholder:text-ink-faint tnum"
+ />
+
+ Записать
+
+
+
+ {weights.value.length > 0 && (
+
+ {[...weights.value].reverse().slice(0, 5).map(item => (
+
+ {dayTitle(item.date)}
+
+ {fmtG(item.kg)}
+ {' '}
+ кг
+
+ removeWeight.mutate({ id: item.id })}
+ >
+
+
+
+ ))}
+
+ )}
+
+
+ Взвешивайтесь утром натощак. Смотрите на тренд за неделю, а не на
+ ежедневные колебания — вода и еда в желудке шумят на ±1 кг.
+
+
+
+ );
+}
diff --git a/kcal/src/ui/icons.tsx b/kcal/src/ui/icons.tsx
new file mode 100644
index 0000000..f19d3d2
--- /dev/null
+++ b/kcal/src/ui/icons.tsx
@@ -0,0 +1,69 @@
+import {
+ Apple,
+ BookOpen,
+ ChartColumn,
+ ChevronLeft,
+ ChevronRight,
+ Download,
+ Plus,
+ ScanBarcode,
+ Search,
+ Trash2,
+ Upload,
+ UserRound,
+ Weight,
+ X,
+} from 'lucide';
+import type { IconNode } from 'lucide';
+
+interface IconProps {
+ class?: string;
+}
+
+/**
+ * Иконки — данные из `lucide` (пары [тег, атрибуты] без обёртки svg).
+ * Компоненты lucide-vue-next построены на vdom и в чистом Vapor не работают,
+ * поэтому рендерим содержимое сами: данные статичны и доверенны — сериализуем
+ * их в разметку один раз на модуль и вставляем через v-html.
+ */
+function toMarkup(node: IconNode): string {
+ return node
+ .map(([tag, attrs]) => {
+ const serialized = Object.entries(attrs)
+ .map(([key, value]) => `${key}="${String(value)}"`)
+ .join(' ');
+ return `<${tag} ${serialized}/>`;
+ })
+ .join('');
+}
+
+function createIcon(node: IconNode) {
+ const markup = toMarkup(node);
+ return (props: IconProps) => (
+
+ );
+}
+
+export const IconBook = createIcon(BookOpen);
+export const IconChart = createIcon(ChartColumn);
+export const IconApple = createIcon(Apple);
+export const IconUser = createIcon(UserRound);
+export const IconPlus = createIcon(Plus);
+export const IconChevronLeft = createIcon(ChevronLeft);
+export const IconChevronRight = createIcon(ChevronRight);
+export const IconClose = createIcon(X);
+export const IconTrash = createIcon(Trash2);
+export const IconSearch = createIcon(Search);
+export const IconScale = createIcon(Weight);
+export const IconBarcode = createIcon(ScanBarcode);
+export const IconDownload = createIcon(Download);
+export const IconUpload = createIcon(Upload);
diff --git a/kcal/src/ui/state.ts b/kcal/src/ui/state.ts
new file mode 100644
index 0000000..29372ea
--- /dev/null
+++ b/kcal/src/ui/state.ts
@@ -0,0 +1,28 @@
+import { reactive, shallowRef } from 'vue';
+import type { Meal } from '../domain/types';
+import { todayISO } from '../domain/dates';
+
+export type Tab = 'diary' | 'stats' | 'foods' | 'profile';
+
+export const activeTab = shallowRef('diary');
+
+/** День, открытый в дневнике. Записи добавляются именно в него. */
+export const selectedDate = shallowRef(todayISO());
+
+/** Шторка добавления записи. */
+export const addSheet = reactive({ open: false, meal: 'breakfast' as Meal });
+
+/** Шторка редактирования записи дневника. */
+export const editEntryId = shallowRef(null);
+
+/** Шторка формы продукта; null в foodId — создание нового. */
+export const foodForm = reactive({ open: false, foodId: null as string | null });
+
+export function openAddSheet(meal: Meal): void {
+ addSheet.meal = meal;
+ addSheet.open = true;
+}
+
+export function goToday(): void {
+ selectedDate.value = todayISO();
+}
diff --git a/kcal/tsconfig.json b/kcal/tsconfig.json
new file mode 100644
index 0000000..413f6b7
--- /dev/null
+++ b/kcal/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@robonen/tsconfig/tsconfig.dom.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "jsxImportSource": "vue-jsx-vapor",
+ "types": ["node", "vite/client"],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src", "vite.config.ts", "eslint.config.ts"]
+}
diff --git a/kcal/vite.config.ts b/kcal/vite.config.ts
new file mode 100644
index 0000000..dda48fc
--- /dev/null
+++ b/kcal/vite.config.ts
@@ -0,0 +1,29 @@
+///
+import { defineConfig } from 'vitest/config';
+import vueJsxVapor from 'vue-jsx-vapor/vite';
+import tailwindcss from '@tailwindcss/vite';
+import { syncEnginePlugin } from 'vue-sync-engine/plugin';
+
+// JSX компилируется сразу в Vapor-код (без interop и virtual DOM).
+// Tailwind v4 подключён как Vite-плагин, конфиг живёт в src/app.css (@theme).
+// syncEnginePlugin собирает дефы в virtual:sync-engine-registry — его требует
+// DevTools-ветка движка в dev-режиме.
+export default defineConfig({
+ plugins: [vueJsxVapor(), tailwindcss(), syncEnginePlugin({ definitions: ['/src/data/defs.ts'] })],
+ optimizeDeps: {
+ // Движок ходит в virtual-модуль — пребандл прятал бы его от плагина.
+ exclude: ['vue-sync-engine'],
+ },
+ define: {
+ __VUE_OPTIONS_API__: 'false',
+ __VUE_PROD_DEVTOOLS__: 'false',
+ __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
+ // Dev-ветки vue-sync-engine (DevTools-панель) остаются в `vite dev`,
+ // вырезаются из прод-сборки.
+ __SYNC_ENGINE_DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
+ },
+ test: {
+ environment: 'node',
+ include: ['src/**/*.test.ts'],
+ },
+});