feat: add kcal app

This commit is contained in:
2026-08-07 13:33:17 +07:00
parent 1ee76faf55
commit c02448f7d9
39 changed files with 6966 additions and 0 deletions
+86
View File
@@ -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`+фильтра), когда
дневник разрастётся.