Files
2026-08-07 13:33:17 +07:00

87 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ккал — локальный дневник питания
Считаете калории — приложение делает это быстрым: запись за 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`+фильтра), когда
дневник разрастётся.