From 6d646df4c9d82b8250683301c315291bb4cd1e4b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 03:09:55 +0000 Subject: [PATCH] Move plan picker back out of the title, into its own secondary chip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the "tap the title" version from the previous PR based on feedback: the title-embedded picker scaled fine but took over the static "Plan" title, which read as less clear than the original segmented control's distinct, separate-control feel — even though that control itself didn't scale past a couple of plans. PlanPicker is now a single chip button ("{plan name} ▾") in its own full-width bar above the day list, in the segmented control's old position, opening the same checkmarked dropdown menu on tap. The title goes back to always being plain generic text ("Plan"), and Layout's header grid no longer needs to reserve space for a title-embedded control now that there isn't one — the reverted grid change also fixes itself, since the chip has a full row to itself and no longer competes with the Completed checkbox for width, giving it more room for long plan names than either earlier design had. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01K2wCTwwVhhkSDxQxWo7UoH --- docs/components.md | 17 ++++-- web/src/components/AppRouter.tsx | 8 ++- web/src/components/Layout.module.css | 10 ---- web/src/components/Layout.tsx | 12 +--- web/src/components/PlanPicker.module.css | 34 ++++++------ web/src/components/PlanPicker.tsx | 71 ++++++++++++------------ 6 files changed, 75 insertions(+), 77 deletions(-) diff --git a/docs/components.md b/docs/components.md index 6d22178..4f06932 100644 --- a/docs/components.md +++ b/docs/components.md @@ -9,7 +9,7 @@ App └── AppRouter └── Layout (root for all routes) ├── header - │ ├── title → PlanPicker on /plan when >1 plan exists, else plain text + │ ├── title (plain text, always — never replaced by route content) │ ├── SearchInput │ ├── show-completed checkbox │ └── menu icon → PlanSettings (sidebar) @@ -18,7 +18,8 @@ App │ ├── / → ChapterGroupList (Books view) │ │ └── ChapterGroup (one per book) │ │ └── Chapter (one per chapter) - │ ├── /plan → ChapterGroupList (Plan view) + │ ├── /plan → PlanPicker (chip + dropdown, hidden if only one plan) + │ │ + ChapterGroupList (Plan view) │ │ └── ChapterGroup (one per day) │ │ └── Chapter │ └── /history → HistoryList @@ -38,11 +39,13 @@ Configures routes. Computes two top-level data structures passed as props: - `bookGroups: Record` — computed once via `groupByBook` - `planGroups: Accessor>` — `createMemo` around `groupByDay`; reactive to `api.perDayTagData()` +The `/plan` route renders `PlanPicker` above `ChapterGroupList`. + ### `Layout` (`Layout.tsx`) Shell shared by all routes. Contains: -- Page title (derived from current path) — on `/plan`, replaced with `PlanPicker` once there's more than one plan to switch between +- Page title (derived from current path) — always plain generic text ("Plan", "Books", "History", "Settings"); route content never repurposes it, by design (see `PlanPicker` below) - Show Completed checkbox (toggles `api.showCompleted`) - Search input (writes to `api.setSearchText`) - Sidebar toggle (shows/hides `PlanSettings`) @@ -50,9 +53,13 @@ Shell shared by all routes. Contains: ### `PlanPicker` (`PlanPicker.tsx`) -iOS-style "tap the title to switch context" menu, the same pattern Mail uses for its inbox picker and Reminders for its list picker — chosen over a segmented control specifically because a segmented control runs out of horizontal room once there are more than a couple of plans. `Layout` only renders it (in place of the plain `

` title text) when `api.plans().length > 1`; the trigger button shows `api.activePlan().name` plus a chevron and, when tapped, opens an absolutely-positioned menu (closed on outside `pointerdown` or on selecting an item) listing every `api.plans()` entry with a checkmark on the active one. Selecting an entry calls `api.setActivePlanId`. +A secondary "current plan" chip rendered above the day list on `/plan` — tapping it opens a checkmarked dropdown menu listing every `api.plans()` entry; selecting one calls `api.setActivePlanId`. Renders nothing (` 1}>`) when there's only one plan. + +This went through two other designs first, both rejected on user feedback after being built and tried: +1. A segmented control (one button per plan) — doesn't scale, runs out of horizontal room past a couple of plans. +2. The chip's current name+chevron content shown *in the page title itself* (replacing "Plan" with e.g. "My Plan ▾") — scaled fine, but the user specifically preferred that a plan switcher read as a distinct secondary control rather than take over the static title. (That version also needed `Layout`'s header grid to reserve space so the picker's trigger wouldn't overlap the Completed checkbox — that grid change is reverted along with it.) -`Layout`'s `.header` is a `1fr auto` grid (title column, then the Completed-checkbox column) and normally lets `.title` span both columns so short static titles (Books/History/Settings/"Plan") stay dead-centered in the full header width — the checkbox sits in its own column but `.title` freely overlaps that space since centered short text never reaches it. `PlanPicker`'s trigger can be much wider (a plan name plus a chevron), so `Layout` adds `styles.titleConstrained` to the `

` only while the picker is showing, confining it to the `1fr` column instead of overlapping the checkbox's column. This is deliberately conditional — the same confinement applied unconditionally would needlessly truncate even short plan names, since it shrinks the box regardless of whether the content actually needs the room (verified: an earlier symmetric-padding attempt truncated even `"My Plan"`). +The current design keeps the title untouched and gives the chip its own full-width bar instead (`padding: 0.6rem var(--item-padding-h); background-color: var(--color-page);`, mirroring the segmented control's old wrapper), which incidentally also gives it much more room than either previous design for long plan names before `text-overflow: ellipsis` kicks in, since it isn't sharing a row with the Completed checkbox at all. It scrolls away with the page content rather than staying pinned, for the same reason the segmented control did: `ChapterGroup`'s own day headers are already `position: sticky; top: 0` inside the same scroll container (`Layout`'s `
`), and that component is shared across the Books/Plan/History routes, so pinning the chip bar at the same offset would visually collide with the day headers once you scroll. ### `ChapterGroupList` (`ChapterGroupList.tsx`) diff --git a/web/src/components/AppRouter.tsx b/web/src/components/AppRouter.tsx index a1628f4..ca36646 100644 --- a/web/src/components/AppRouter.tsx +++ b/web/src/components/AppRouter.tsx @@ -3,6 +3,7 @@ import { Layout } from './Layout' import { ChapterGroupList } from './ChapterGroupList' import { HistoryList } from './HistoryList' import { PlanSettings } from './PlanSettings' +import { PlanPicker } from './PlanPicker' import { getBookNamesMap, getChapterData } from '../utils/dataUtils' import { groupByBook, groupByDay } from '../utils/groupUtils' import { useApi } from './ApiContext' @@ -28,7 +29,12 @@ export function AppRouter() { /> } + component={() => ( + <> + + + + )} /> props.location.pathname.endsWith('/settings') - const isPlanRoute = () => props.location.pathname.endsWith('/plan') - const showPlanPicker = () => isPlanRoute() && api.plans().length > 1 const title = () => - isPlanRoute() + props.location.pathname.endsWith('/plan') ? 'Plan' : props.location.pathname.endsWith('/history') ? 'History' @@ -52,11 +48,7 @@ export function Layout(props: RouteSectionProps) {
-

- - - -

+

{title()}