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'sgoalPrefix(defaults to English"of"; a German consumer passesgoal-prefix="von"), orMealEntry's already-formattedtime/portionLabelstrings. - 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.