Plan editor UX: time picker, day-pager polish, day-range mismatch banner#31
Open
infinitea1 wants to merge 5 commits into
Open
Plan editor UX: time picker, day-pager polish, day-range mismatch banner#31infinitea1 wants to merge 5 commits into
infinitea1 wants to merge 5 commits into
Conversation
The current serve_cmd uses bash-style env-var prefix and line continuations
(KEY="value" \). That works on macOS where Tilt runs commands through bash,
but on Windows Tilt invokes serve_cmd via `cmd /S /C`, which doesn't parse
bash continuations or env prefixes — so `tilt up` can't boot the backend on
a Windows machine.
Switch to Tilt's serve_env={...} dict (platform-agnostic env wiring) and
swap the pre-built bin/server invocation for `go run ./cmd/server` (no
OS-specific binary suffix). Same env vars, same behaviour on macOS — also
works on Windows now.
Also gitignore CLAUDE.md / CLAUDE.local.md / .claude/ for personal
agent-tooling files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace "Drop items here to unassign them" with clearer wording about removing items from a day, since "unassign" is jargon that doesn't explain the actual interaction to users. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the bare native time input on plan items with a custom TimePicker that opens a portal-based popover with separate hour and minute (5-min granularity) columns. The trigger keeps a native time input so typing still works and Firefox users — who get no clock UI from the native input — finally have a clickable picker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add "Day X of N" count to the day header so users see how many days exist at a glance. - Replace the prev/next chevron buttons with circular tinted controls whose disabled state reads as inactive (transparent bg, 30% opacity). - Lift the day header out of the per-card carousel into a single shared bar above the scrollable container. The chevrons now stay in a fixed screen position regardless of scroll progress, so rapid clicks always register and the carousel slides cleanly underneath. - Replace the 800ms-timeout flag-flip in the scroll-sync effect with a precise scrollend listener (with a 1500ms safety-net timeout for browsers that don't support scrollend). Eliminates a race where the onScroll listener could mid-animation set selectedDayId to a different day, causing chevrons to feel stuck. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the existing plan-days don't match the plan's start/end dates (e.g. user edited the dates after generating days), show an always-on banner above the itinerary with two actions: - "Add N days" — POSTs only the missing dates to /v1/plans/:id/days, leaving any existing days untouched. - "Remove N (X items → scratchpad)" — DELETEs the orphan days. Items on those days move to the scratchpad automatically thanks to the ON DELETE SET NULL on plan_items.plan_day_id; the button label tells the user how many items will move. The empty-state "+ Generate Days from Dates" button now routes through the same handler (handleAddMissingDays), which generalises cleanly since "all dates missing" is just one case of the partial mismatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
samrford
reviewed
May 4, 2026
Owner
There was a problem hiding this comment.
Would be good to use this new component inside DateTimePicker (replacing the time part with this - that we we have the same timepicker component across the app
samrford
reviewed
May 4, 2026
| for (const date of days) { | ||
| const newDay = await apiFetch<PlanDay>(`/v1/plans/${id}/days`, { | ||
| for (const key of missingDateKeys) { | ||
| await apiFetch<PlanDay>(`/v1/plans/${id}/days`, { |
Owner
There was a problem hiding this comment.
I think we should use Promise.allSettled here so all requests fire in parallel rather than sequentially, and you can report what actually happened.
I think we should add a little helper for retrying in api.ts so we don't just fail outright if there's an error in any of the requests:
// in lib/api.ts (next to apiFetch):
export async function withRetry<T>(
fn: () => Promise<T>,
{ retryCount = 3, baseDelayMs = 300 } = {},
): Promise<T> {
let lastErr: unknown;
for (let i = 0; i <= retryCount; i++) {
try {
return await fn();
} catch (e) {
lastErr = e;
// Don't retry 4xx — they won't get better on retry.
if (e instanceof ApiError && e.status >= 400 && e.status < 500) throw e;
if (i < retryCount) {
await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** i));
}
}
}
throw lastErr;
}Then the handler in this file becomes:
const results = await Promise.allSettled(
missingDateKeys.map((key) =>
withRetry(() =>
apiFetch<PlanDay>(`/v1/plans/${id}/days`, {
method: "POST",
body: JSON.stringify({ date: `${key}T00:00:00Z` }),
}),
),
),
);
queryClient.invalidateQueries({ queryKey: planKeys.detail(id) });
const ok = results.filter((r) => r.status === "fulfilled").length;
const failed = results.length - ok;
if (failed === 0) {
toast.success(`Added ${ok} day${ok === 1 ? "" : "s"}`);
} else if (ok === 0) {
toast.error("Failed to add missing days");
} else {
toast.error(`Added ${ok} of ${results.length} days — ${failed} failed`);
}Same shape applies to handleDeleteOrphanDays below
samrford
reviewed
May 4, 2026
| ) : ( | ||
| <div className="flex flex-col items-center justify-center h-full py-4 text-center pointer-events-none"> | ||
| <p className="text-gray-500 dark:text-gray-400 text-sm">{isOwner ? "Drop items here to unassign them, or add new ideas!" : "No unassigned items."}</p> | ||
| <p className="text-gray-500 dark:text-gray-400 text-sm">{isOwner ? "Drag items here to remove them from a day, or add new ideas!" : "No unassigned items."}</p> |
Owner
There was a problem hiding this comment.
Suggested change
| <p className="text-gray-500 dark:text-gray-400 text-sm">{isOwner ? "Drag items here to remove them from a day, or add new ideas!" : "No unassigned items."}</p> | |
| <p className="text-gray-500 dark:text-gray-400 text-sm">{isOwner ? "Drag items here to unassign them, or add new ideas by clicking the button above!" : "No unassigned items."}</p> |
I like the word unassign here :D
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on top of # (
fix-tilt-cross-platform). When thatmerges, GitHub will auto-retarget this to
master.Four small, independent changes to the plan editor — split into one commit
each so anything can be reverted on its own:
feat(plan): clarify scratchpad drop-zone copy— replaces "Drop itemshere to unassign them" with clearer wording about removing items from a day.
"Unassign" was jargon that didn't explain the actual interaction.
feat(plan-item): two-column time picker with native fallback— newTimePickercomponent with hour + minute (5-min) columns in a portalpopover. The trigger keeps a native
<input type="time">so typing stillworks and Firefox users (whose native input has no clock UI) finally have
a clickable picker.
feat(plan-itinerary): improve day-pager UX— adds "Day X of N" count,restyled circular chevrons, and lifts the day header out of each carousel
card into a single shared bar above the scrollable container. Most
opinionated change in this PR — the chevrons now stay in a fixed screen
position regardless of scroll, so rapid clicks always register.
Also replaces the 800ms-timeout flag-flip in the scroll-sync effect with
a precise
scrollendlistener (with a 1500ms timer fallback).feat(plan-itinerary): surface day-range mismatch banner— when theplan's start/end dates don't match its existing days (e.g. user edited
dates after generating), an always-on banner offers "Add N days" and
"Remove N (X items → scratchpad)". Items on removed days move to the
scratchpad automatically thanks to the existing
ON DELETE SET NULL.Test plan
picker popover opens, both columns scroll, typing into the input still
works.
chevrons cycle correctly, disabled state on Day 1 / Day N is obvious.
the last day with no wobble or stuck-chevron state.
appears with "Add N days" → click adds only the missing dates.
days → banner says "Remove N (X items → scratchpad)" → click removes
the days and items appear in the scratchpad.