Files
spine/docs/specs/2026-06-21-concise-default-view-design.md

3.3 KiB

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).