Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions docs/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,27 +39,33 @@ Configures routes. Computes two top-level data structures passed as props:
- `bookGroups: Record<BookName, ChapterData[]>` — computed once via `groupByBook`
- `planGroups: Accessor<Record<string, ChapterData[]>>` — `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 (`<Show when={api.plans().length > 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 (`<Show when={api.plans().length > 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 `<main>`), 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 `<main>`), 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`)

Expand All @@ -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`)

Expand Down
9 changes: 6 additions & 3 deletions web/src/components/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -24,14 +24,17 @@ export function AppRouter() {
<Route
path="/"
component={() => (
<ChapterGroupList data={bookGroups} sortProgressToTop />
<>
<ListToolbar />
<ChapterGroupList data={bookGroups} sortProgressToTop />
</>
)}
/>
<Route
path="/plan"
component={() => (
<>
<PlanPicker />
<ListToolbar showPlanPicker />
<ChapterGroupList data={planGroups()} />
</>
)}
Expand Down
4 changes: 4 additions & 0 deletions web/src/components/ChapterGroup.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion web/src/components/ChapterGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ export function ChapterGroup(props: ChapterGroupProps) {
<span class={styles.completedCount}>({completedCount()})</span>
</Show>
{chapters().length}
<Icon class={accordionStyles.icon} name="chevron-down-sharp" />
<Icon
class={className(
accordionStyles.icon,
chapters().length === 0 && styles.chevronDisabled,
)}
name="chevron-down-sharp"
/>
</span>

<ol class={className(styles.chapterList, accordionStyles.content)}>
Expand Down
47 changes: 0 additions & 47 deletions web/src/components/Layout.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
8 changes: 0 additions & 8 deletions web/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,6 @@ export function Layout(props: RouteSectionProps) {
<header class={styles.header}>
<h1 class={styles.title}>{title()}</h1>
<Show when={!isSettingsRoute()}>
<label class={styles.showCompleted}>
<input
type="checkbox"
checked={api.showCompleted()}
onChange={() => api.toggleShowCompleted()}
/>
Completed
</label>
<SearchInput class={styles.search} onSearch={api.setSearchText} />
</Show>
</header>
Expand Down
45 changes: 45 additions & 0 deletions web/src/components/ListToolbar.module.css
Original file line number Diff line number Diff line change
@@ -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);
}
}
29 changes: 29 additions & 0 deletions web/src/components/ListToolbar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div class={styles.ListToolbar}>
<Show when={props.showPlanPicker}>
<PlanPicker />
</Show>
<label class={styles.showCompleted}>
<input
type="checkbox"
checked={api.showCompleted()}
onChange={() => api.toggleShowCompleted()}
/>
Completed
</label>
</div>
)
}
6 changes: 3 additions & 3 deletions web/src/components/PlanPicker.module.css
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
Loading