Skip to main content
← Blog

How to show 'in progress' — from alarm to end

VauDium ·

If focus is a blue outer wrap, how should 'in progress' look? I thought one line about STARTED would settle it. We ended up debating where it should even begin to show up.

How to show ‘in progress’

A Fecit task row already carries a few signals. The star’s color encodes fidelity. A yellow right edge says a retrospect exists. A blue outer wrap means it’s focused. Each meaning gets its own spot.

Today added one more — a marker that says this is happening right now. Visually similar to the focus wrap, but red. I expected it to be simple.

Defining “in progress”

First I had to decide what in progress even meant.

The obvious answer was the STARTED state — a task whose state field got flipped by the user pressing “start.” Clean, but it didn’t survive the use cases.

Picture a calendar entry. There’s a 10:00 meeting. At 9:55 the reminder goes off, and the user walks back to their desk. By 10:00 sharp, no one is going to press “start.” For scheduled tasks, being inside the scheduled window is the natural in-progress state — you don’t manually toggle it each time.

The marker needs to be on screen during that whole window. Even when STARTED isn’t set.

The starting point is the alarm

So the trigger became time, not state. Show the marker from the task’s start time.

Then one more nudge felt right — start it at the alarm time instead. That’s startDate shifted earlier by reminderOffset minutes. The moment the reminder fires, the marker appears, and the user sees the visual confirmation: the window is open now.

const inProgressStart = useMemo(() => {
    if (!task.startDate) return undefined;
    const offsetMin = task.reminderOffset ?? 0;
    return new Date(task.startDate.getTime() - offsetMin * 60_000);
}, [task.startDate, task.reminderOffset]);

If reminderOffset isn’t set, fall back to startDate. “No alarm? Then from the start time itself.” Skipping the marker just because there’s no alarm felt wrong.

The ending point

When does it disappear? My first answer was when completed or cancelled. But that’s incomplete — once endDate passes, the task isn’t currently in progress anymore, regardless of whether the user got around to finishing. The marker means “inside this window,” not “until this person closes it.”

The final rule:

  • Appear: now >= alarmTime
  • Disappear: now > endDate, or COMPLETED/CANCELLED
  • Required: both startDate and endDate set (without an endDate, until when is undefined)

The condition re-evaluates every minute via nowMinuteAtom — a jotai atom that ticks each minute. Minute-grained precision is enough here.

Focus wins

Once this was working, an overlap showed up. A task that’s both focused (blue outer wrap) and in progress (red outer wrap) gets two competing borders.

Focus is a deliberate statement by the user: “I’m working on this one now.” The in-progress marker is a factual statement about time: “we’re inside the scheduled window.” Both are forms of active, but they live on different layers. Stacking them visually muddles the hierarchy — two colored lines competing for the same edge.

The rule: if focus is on, suppress the red marker. Focus is the user’s active choice; the red marker is a time-driven default. Active intent outranks automatic state.

const isInProgress = useMemo(() => {
    if (isFocused) return false; // focus marker takes precedence
    if (displayState === COMPLETED || displayState === CANCELLED) return false;
    if (!inProgressStart || !task.endDate) return false;
    return now >= inProgressStart && now <= task.endDate;
}, [...]);

Mobile and desktop together

The same rule shipped to both platforms in one go. Mobile uses an absoluteFillObject overlay in React Native; desktop uses Tailwind’s absolute inset-0. Implementation differs, but the visual is identical: a 1.5px DANGER500 outer border.

The API already has a computed reminder_at field, so I double-checked that the client formula doesn’t diverge from the server’s. Both use the same expression — startDate - reminderOffset minutes, defaulting offset to 0. Deterministic, no drift.

Looking back

A single phrase — “in progress” — seemed like one boolean. Pulling on it surfaced at least five forks: state-based or time-based, what point does it start at, where does it end, what does it require to even show, and what happens when it overlaps another marker.

Each fork was a tiny decision, and each one revealed a fragment of what Fecit is. Anchoring to the alarm time came from the calendar’s premise — scheduled time is execution time. Letting focus override the marker came from a principle — active user intent beats automatic state.

I thought I was just picking a color. By the end it was a series of short lines drawn between intent and automation, between state and time.