Skip to content
Ritesh FirodiyaGet in touch

Work / AskCal / Wiki / Surfaces

calendar

Surfacecanonicalverified 2026-09-27

SURFACE.MOBILE.HOME.CALENDAR

Summary

A month of day rings, opened from Today's date, so a day older than seven is reachable at all.

Raw wireframe

  • .context/designs/mobile/home/calendar.html

Why it is drawn this way

It exists because the navigation stopped at a week and the data did not. Nothing is ever deleted — meals live in MMKV for ever and Export everything as CSV dumps all of them — but every dated surface in the app was capped at seven in four separate places: recentDays(now, count = 7), the strip that renders it, the Trends chart's own for (let i = 6; …), and the when-sheet's backdating list. The only unbounded surface was the gallery, which reaches an old day only if that day was photographed: photoDays filters on photoUri, so a meal logged by search or barcode, or any meal at all once keepMealPhotos is off, had no route back. See today.

It is a sheet over Today, not a route, and not a tab. today.tsx is one route holding day in local state; the strip re-renders it rather than navigating. A pushed route would have to hand a day back to a tab it does not own — the shape of bug recorded in CLAUDE.md against what was then the Add tab (the tab is gone; food/search.tsx is the screen it was), where scan/correct.tsx did router.replace("/(tabs)/add") and logged a separate meal. Picking a day here is the same setDay() the strip makes, so there is nothing to hand back.

The date is the affordance, not a new icon. The header already names the day you are looking at; a chevron under it is the whole disclosure, and the corner stays free. The same header is drawn in all three of Today's files, because it is one component and a control that exists in one state and not another is drift rather than design.

Sunday first, and that is a code fact rather than a taste. constants/day.ts is ["S","M","T","W","T","F","S"], indexed straight off Date.getDay(). A Monday-first grid needs a week-start constant the app does not have and cannot derive — toLocaleDateString does not report one — so it would be a guess dressed as a locale. Sunday-first reuses the table that already ships.

The ring is the strip's ring, unchanged. Fill is the share of the target, --danger past it, .is-blank for a day with nothing logged, a filled disc for the selected day. See today for why a ring is right here and wrong on the day card.

Days after today are drawn and not tappable. A month missing its last ten days stops looking like a month; a tappable future day invites logging food nobody has eaten. Same rule as the strip ending at today rather than centring it. .dayring.is-future dims the ring and drops the fill, and the cell is a <span>.

› is struck through at the current month rather than removed, the same as the chrome bar's own back/next at the ends of the set: a control that disappears at the edge of its range slides the title sideways, and "there is no next month" has to look like a reason. ‹ stops at the month of the first meal ever logged, so paging back cannot run off into blank years.

A month step is a <button>, not a link. It re-renders this sheet against a different month. Nothing navigates, which is why calendar is a state reached through the bar's switcher and not a second screen.

The count under the grid is the grid read back as a sentence — sixteen rings with a fill, three without. It deliberately claims no figure the grid does not already draw: a monthly average would be a new derivation, and Trends is where a number that is not on screen belongs.

Today is a separate action from closing. The backdrop closes without picking and lands on whichever day was already selected, so after paging back to March there has to be one tap that means "back to now".

What this does not fix

The when-sheet still offers seven days for backdating (meal-when-sheet.tsx:76). Reaching an old day works, and adding to it from Today works because the free paths file against the selected day — but moving an already-logged meal more than a week back is still out of reach. Same fix, different sheet, and the obvious next one.

Trends WAS the other half of this and no longer is: its chart scrolls over the same history this sheet pages through. See trends.

Empty — a month with nothing logged

It is the one place the sheet has to say that a blank day is still actionable. Every ring here is a link, and Today's search and barcode paths file against whichever day is selected — so a lapsed month is somewhere you can still fill in. Without the line saying so, a wall of empty rings reads as "there is nothing here" instead of "you logged nothing here", and the user stops paging.

It is reachable, not hypothetical. ‹ stops at the month of the first meal ever logged, so a completely blank month only exists between two months that have one. That is what lapsing for a few weeks looks like, and it is the commonest thing a tracker's history contains.

The leading cells are empty, not greyed dates from July. A date belonging to another month is either tappable — and then the month title is lying about what you are looking at — or it is not, and then it is decoration people try to tap anyway.

No .empty illustration. The grid is still the content: thirty-one real, tappable days. Replacing it with a centred icon and a headline would remove the only thing on the screen the user can act on, which is the mistake the gallery's own empty state avoids by keeping its one button. See gallery.

Related

Sources

  • .context/designs/mobile/home/calendar.html
  • apps/mobile/src/components/today/calendar-sheet.tsx
  • apps/mobile/src/lib/month.ts
  • apps/mobile/src/lib/day.ts
  • apps/mobile/src/components/day-strip.tsx
  • apps/mobile/src/lib/gallery.ts

Every project of mine is written down like this.

Read the résumé