# Ккал — локальный дневник питания

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