docs: add concise default view design spec

This commit is contained in:
2026-06-21 08:34:58 -04:00
parent b6b0539b82
commit fbe49d729b
@@ -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).