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