diff --git a/docs/specs/2026-06-21-concise-default-view-design.md b/docs/specs/2026-06-21-concise-default-view-design.md new file mode 100644 index 0000000..5014a6b --- /dev/null +++ b/docs/specs/2026-06-21-concise-default-view-design.md @@ -0,0 +1,97 @@ +# Concise Default View — design spec + +## Problem + +The index page shows every book grouped by status. As the library grows, this becomes +too much information at a glance. The user needs a focused default showing only what's +active right now, with easy paths to browse the rest. + +## Design + +The index view takes an optional `filter` query parameter. With no filter, it shows a +concise default: only books in-flight (READING) and the 5 most recently added WANT +books. All other books are accessible via filter links. With a filter (`all`, `read`, +`want`), it shows the full matching subset. + +## Behaviour + +### Default / no filter (concise view) + +- **READING group**: all READING books (unchanged from current). +- **WANT group**: sorted by `:ADDED:` date descending, limited to 5. +- **READ / ABANDONED**: replaced by summary links — e.g. "Read · 3" as a clickable + link to `/index?filter=read`. +- **Filter nav bar** shown at the top: links to `All books (N)`, `Want list`, + `Read (N)`. + +### `?filter=all` + +Full index, all groups, no truncation — identical to today's default. + +### `?filter=read` + +Only the READ group. Single group in the listing. + +### `?filter=want` + +Only the WANT group. Single group in the listing. + +### Error handling + +- An unrecognised/empty filter value is treated as `nil` (concise default). +- A WANT book with no `:ADDED:` property sorts before dated entries (treated as + oldest). +- Fewer than 5 WANT books → all are shown, no padding or placeholder rows. + +## Implementation + +### Model: `spine-index-model` + +Signature changes from: +``` +spine-index-model (books &optional selected-id) +``` +to: +``` +spine-index-model (books &optional filter selected-id) +``` + +When `filter` is nil, after building the group alist: +- Sort the `:want` group entries by `added` descending (ISO date string; nils sort + first = oldest). +- Truncate to 5 entries. +- Replace `:read` and `:abandoned` groups with a `:summary-groups` list. Each + entry is a plist matching the existing model convention: + ``` + (:label "read" :count 3 :href "/index?filter=read") + ``` + The template renders these as clickable `.row` links. + When `filter` is non-nil, `:summary-groups` is absent. +When `filter` is non-nil, return only the matching group (or all groups for `"all"`). + +### Handler: `defservlet index` + +Read the `filter` key from the request query alist. Pass it to `spine-index-model`. +Pass `current_filter` in the template context for conditional rendering. + +### Template: `index.mustache` + +- **Filter bar** (above groups): shown when `current_filter` is nil (links to expanded + views) or truthy (link back to concise view). +- **Summary groups**: new `{{#summary_groups}}` block renders a simple `.row` link per + suppressed group (READ, ABANDONED). +- Existing group rendering unchanged — still handles full listing when `filter=all` + or single-group views. + +### Sorting + +Within the WANT group for the concise view: sort by `added` field descending. The +`added` field is already part of each book's model entry. Nil dates sort before dated +entries (oldest). + +## Out of scope + +- Pagination for large WANT lists (5 is a design choice; if the user wants more, the + "All books" link is there). +- Remembering the user's last selected filter across requests (stateless is simpler). +- Keyboard-driven filter switching (plain links for now).