Work / AskCal / Wiki / Surfaces
calendar
Surfacecanonicalverified 2026-09-27
SURFACE.MOBILE.HOME.CALENDARSummary
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é