# Ккал — локальный дневник питания Считаете калории — приложение делает это быстрым: запись за 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`+фильтра), когда дневник разрастётся.