diff --git a/docs/components.md b/docs/components.md index 4f06932..2197965 100644 --- a/docs/components.md +++ b/docs/components.md @@ -11,14 +11,14 @@ App ├── header │ ├── title (plain text, always — never replaced by route content) │ ├── SearchInput - │ ├── show-completed checkbox │ └── menu icon → PlanSettings (sidebar) │ └── TagSelector (one per PerDayTagData entry) ├── main (route outlet) - │ ├── / → ChapterGroupList (Books view) + │ ├── / → ListToolbar (Completed checkbox only) + │ │ + ChapterGroupList (Books view) │ │ └── ChapterGroup (one per book) │ │ └── Chapter (one per chapter) - │ ├── /plan → PlanPicker (chip + dropdown, hidden if only one plan) + │ ├── /plan → ListToolbar (Completed checkbox + PlanPicker chip) │ │ + ChapterGroupList (Plan view) │ │ └── ChapterGroup (one per day) │ │ └── Chapter @@ -39,27 +39,33 @@ 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`. +The `/` and `/plan` routes each render `ListToolbar` above `ChapterGroupList` (`/plan` passes `showPlanPicker`). `/history` doesn't get a toolbar — `HistoryList` never reads `api.showCompleted()`, so there'd be nothing for the checkbox to do there. ### `Layout` (`Layout.tsx`) Shell shared by all routes. Contains: -- 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`) +- Page title (derived from current path) — always plain generic text ("Plan", "Books", "History", "Settings"); route content never repurposes it - Search input (writes to `api.setSearchText`) - Sidebar toggle (shows/hides `PlanSettings`) - Tab bar navigation (Plan, Books, History, Settings) +The Completed checkbox used to live here too, styled to sit at the header's top-right. It's now owned by `ListToolbar` instead — see below. + +### `ListToolbar` (`ListToolbar.tsx`) + +Secondary bar rendered above a chapter list (`background-color: var(--color-page); padding: 0.6rem var(--item-padding-h);`), holding the Completed checkbox and, on `/plan` only (`showPlanPicker` prop), the `PlanPicker` chip. Both moved here from `Layout`'s header — the checkbox to put it in the same container as the plan picker, and to make it easy to show that same bar on the Books view even though Books has no plan to pick. Layout: `display: flex`, `PlanPicker` (if present) on the left, the checkbox pushed to the far right via `margin-left: auto` on `.showCompleted` — that works whether or not `PlanPicker` is rendered, so the checkbox's position doesn't shift between `/` and `/plan`. + ### `PlanPicker` (`PlanPicker.tsx`) -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. +A "current plan" chip — 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. Rendered inside `ListToolbar`, not standalone — its own stylesheet only owns the chip and its dropdown menu now, not the bar around it. -This went through two other designs first, both rejected on user feedback after being built and tried: +This went through three designs, each replaced 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.) +2. The chip's name+chevron shown *in the page title itself* (replacing "Plan" with e.g. "My Plan ▾") — scaled fine, but read as taking over the static title rather than acting as a distinct secondary control, and needed `Layout`'s header grid to reserve space so the trigger wouldn't overlap the Completed checkbox. +3. The chip in its own full-width bar (mirroring the segmented control's old wrapper), title left alone — the current design. Landing the checkbox in that same bar (rather than the header) was a follow-up refinement, at which point the bar became `ListToolbar` (shared with the Books view) instead of something `PlanPicker` owned outright. -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. +The chip's bar 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 toolbar at the same offset would visually collide with the day headers once you scroll. ### `ChapterGroupList` (`ChapterGroupList.tsx`) @@ -83,7 +89,7 @@ Displays a `CheckMark` with three states: `complete` (all chapters read), `parti On `onChange` from a child `Chapter`, re-runs `filteredChapters()` to update the list immediately. -The header is only expandable when `chapters().length > 0` — mirrors the same guard `Chapter.tsx` uses for its own date-history accordion (`onExpanderClick`/`dates().length === 0`). This matters when "Completed" is hidden: a group can have all its chapters filtered out (fully read) while still being listed (`ChapterGroupList`'s own group-visibility filter only checks the search text, not completion), so without the guard its header would toggle open to an empty list. A `createEffect` also force-collapses the group if it's expanded and its filtered count drops to 0 (e.g. toggling "Completed" off while a fully-read group is open). +The header is only expandable when `chapters().length > 0` — mirrors the same guard `Chapter.tsx` uses for its own date-history accordion (`onExpanderClick`/`dates().length === 0`). This matters when "Completed" is hidden: a group can have all its chapters filtered out (fully read) while still being listed (`ChapterGroupList`'s own group-visibility filter only checks the search text, not completion), so without the guard its header would toggle open to an empty list. A `createEffect` also force-collapses the group if it's expanded and its filtered count drops to 0 (e.g. toggling "Completed" off while a fully-read group is open). The chevron gets `styles.chevronDisabled` (`color: var(--color-muted); opacity: 0.4;`) in that same state, so a non-interactive row also looks non-interactive. ### `Chapter` (`Chapter.tsx`) diff --git a/web/src/components/AppRouter.tsx b/web/src/components/AppRouter.tsx index ca36646..ed7986b 100644 --- a/web/src/components/AppRouter.tsx +++ b/web/src/components/AppRouter.tsx @@ -3,7 +3,7 @@ import { Layout } from './Layout' import { ChapterGroupList } from './ChapterGroupList' import { HistoryList } from './HistoryList' import { PlanSettings } from './PlanSettings' -import { PlanPicker } from './PlanPicker' +import { ListToolbar } from './ListToolbar' import { getBookNamesMap, getChapterData } from '../utils/dataUtils' import { groupByBook, groupByDay } from '../utils/groupUtils' import { useApi } from './ApiContext' @@ -24,14 +24,17 @@ export function AppRouter() { ( - + <> + + + )} /> ( <> - + )} diff --git a/web/src/components/ChapterGroup.module.css b/web/src/components/ChapterGroup.module.css index 80e4698..55d324c 100644 --- a/web/src/components/ChapterGroup.module.css +++ b/web/src/components/ChapterGroup.module.css @@ -22,6 +22,10 @@ color: var(--color-muted); font-size: 1rem; } + .chevronDisabled { + color: var(--color-muted); + opacity: 0.4; + } .chapterList { border-bottom: 1px solid var(--color-border); display: none; diff --git a/web/src/components/ChapterGroup.tsx b/web/src/components/ChapterGroup.tsx index a5b4eb0..f82ff3b 100644 --- a/web/src/components/ChapterGroup.tsx +++ b/web/src/components/ChapterGroup.tsx @@ -80,7 +80,13 @@ export function ChapterGroup(props: ChapterGroupProps) { ({completedCount()}) {chapters().length} - +
    diff --git a/web/src/components/Layout.module.css b/web/src/components/Layout.module.css index 8cb93b0..3e9f8a7 100644 --- a/web/src/components/Layout.module.css +++ b/web/src/components/Layout.module.css @@ -4,65 +4,18 @@ height: 100%; .header { - display: grid; background-color: var(--color-primary); border-bottom: 1px solid var(--color-border); - align-items: center; - grid-template-columns: 1fr auto; padding: calc(20px + env(safe-area-inset-top)) 1rem 20px; } .title { - grid-column: 1 / -1; - grid-row: 1; color: var(--color-on-primary); font-size: 34px; line-height: 36px; margin: 0; text-align: center; } - .showCompleted { - grid-column: 2; - grid-row: 1; - color: var(--color-on-primary); - display: flex; - gap: 0.2rem; - height: 36px; - align-items: center; - justify-self: end; - - input[type='checkbox'] { - appearance: none; - -webkit-appearance: none; - width: 20px; - height: 20px; - border: 2px solid var(--color-hover); - border-radius: 4px; - background: var(--color-surface); - position: relative; - cursor: pointer; - } - - input[type='checkbox']:checked { - background: var(--color-accent); - border-color: var(--color-accent); - } - - input[type='checkbox']:checked::after { - content: ''; - display: block; - position: absolute; - left: 5px; - top: 1px; - width: 6px; - height: 12px; - border: solid var(--color-on-accent); - border-width: 0 2px 2px 0; - transform: rotate(45deg); - } - } .search { - grid-column: 1 / -1; - grid-row: 2; width: 100%; margin-top: 0.5rem; } diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index c46c83c..79e7523 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -50,14 +50,6 @@ export function Layout(props: RouteSectionProps) {

    {title()}

    -
    diff --git a/web/src/components/ListToolbar.module.css b/web/src/components/ListToolbar.module.css new file mode 100644 index 0000000..78537b5 --- /dev/null +++ b/web/src/components/ListToolbar.module.css @@ -0,0 +1,45 @@ +.ListToolbar { + display: flex; + align-items: center; + gap: 0.75rem; + background-color: var(--color-page); + padding: 0.6rem var(--item-padding-h); +} + +.showCompleted { + margin-left: auto; + display: flex; + gap: 0.2rem; + align-items: center; + flex-shrink: 0; + + input[type='checkbox'] { + appearance: none; + -webkit-appearance: none; + width: 20px; + height: 20px; + border: 2px solid var(--color-hover); + border-radius: 4px; + background: var(--color-surface); + position: relative; + cursor: pointer; + } + + input[type='checkbox']:checked { + background: var(--color-accent); + border-color: var(--color-accent); + } + + input[type='checkbox']:checked::after { + content: ''; + display: block; + position: absolute; + left: 5px; + top: 1px; + width: 6px; + height: 12px; + border: solid var(--color-on-accent); + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } +} diff --git a/web/src/components/ListToolbar.tsx b/web/src/components/ListToolbar.tsx new file mode 100644 index 0000000..fe7f02d --- /dev/null +++ b/web/src/components/ListToolbar.tsx @@ -0,0 +1,29 @@ +import { Show } from 'solid-js' +import { useApi } from './ApiContext' +import { PlanPicker } from './PlanPicker' +import styles from './ListToolbar.module.css' + +export interface ListToolbarProps { + showPlanPicker?: boolean +} + +/** Secondary bar above a chapter list — the plan picker chip (Plan view only) plus the Completed checkbox, both previously in the header. */ +export function ListToolbar(props: ListToolbarProps) { + const api = useApi() + + return ( +
    + + + + +
    + ) +} diff --git a/web/src/components/PlanPicker.module.css b/web/src/components/PlanPicker.module.css index 429859d..f91e16b 100644 --- a/web/src/components/PlanPicker.module.css +++ b/web/src/components/PlanPicker.module.css @@ -1,7 +1,7 @@ .PlanPicker { position: relative; - background-color: var(--color-page); - padding: 0.6rem var(--item-padding-h); + min-width: 0; + max-width: 100%; } .chip { @@ -39,7 +39,7 @@ .menu { position: absolute; top: 100%; - left: var(--item-padding-h); + left: 0; margin-top: 0.4rem; background-color: var(--color-surface); border: 1px solid var(--color-border);