Skip to content

Architecture: presentation-only

Every component in yemek follows the same contract, no exceptions:

What a component owns

  • Rendering a value/state it's given, with an animated transition on change.
  • Zone/status coloring — components accept a status (or similar) prop and pick the matching color token; they never decide what counts as on-track/over/under.
  • Layout and motion for its own markup.

What a component never owns

  • Domain logic. Calorie balance, streak/tier classification, portion math, macro calculations — all of that lives in the consuming app (in meal-tracker's case, app/lib/) and is passed in already computed. A component never re-derives a total, a percentage, or a classification from raw inputs.
  • Locale/language. No hardcoded strings. Anything user-facing that varies by language is a prop — see MetricRing's goalPrefix (defaults to English "of"; a German consumer passes goal-prefix="von"), or MealEntry's already-formatted time/portionLabel strings.
  • Date/number formatting beyond what's explicitly documented as a display convenience (e.g. a ring rounds and locale-formats its own number for the pop animation, but a chart's axis labels are always pre-formatted strings the consumer supplies).

Why

This is what lets the same MetricRing render meal-tracker's calorie ring (which has a cut/bulk-relative "over" direction) and its protein ring (a floor, no "over" direction that means anything) — the component just renders status, and each call site supplies whatever judgment call is correct for that metric.

It also means yemek can be dropped into a different app with a different language and a different domain entirely (a different nutrition philosophy, different goal semantics) without a single line of yemek code changing — only the props passed in change.

Composables and state

Presentation-only doesn't mean stateless. A few components own small pieces of UI-only state — TrendChart's selectedIndex (which point is highlighted), StreakHeatmap's daily celebration-shown tracking. That's fine: it's transient, resets on remount, and never crosses into domain judgment.

If yemek grows composables (e.g. for a future compound component made of multiple parts sharing implicit state via provide/inject), the same boundary applies: a composable may own open/selected/focused-style UI state, never business logic. See the project's CLAUDE.md §1 for the canonical rule.