docs: add mustache templating design spec
This commit is contained in:
@@ -0,0 +1,137 @@
|
|||||||
|
# Spine — Mustache Template Rendering
|
||||||
|
|
||||||
|
Replaces inline HTML generation with Mustache template rendering via
|
||||||
|
`spine-render`. The mockup-A mustache template becomes the canonical
|
||||||
|
index page template. A view-model builder function transforms `spine-books`
|
||||||
|
plists into the template's expected context shape.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
spine.org ──→ [spine-books] ──→ book plists
|
||||||
|
│
|
||||||
|
[spine-index-model] ──→ view model (ht)
|
||||||
|
│
|
||||||
|
templates/index.mustache ───────────┼──→ [spine-render] ──→ HTML
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`spine-mockup-a-agenda.mustache` moves to `templates/index.mustache`.
|
||||||
|
|
||||||
|
The template already contains:
|
||||||
|
- Pico CSS v2.1.1 CDN link
|
||||||
|
- Tabler Icons CDN link
|
||||||
|
- All custom CSS in `<style>` block (agenda layout, pills, detail, dark mode)
|
||||||
|
- Mustache sections: `{{#groups}}`, `{{#books}}`, `{{#detail}}`, `{{#notes}}`, `{{#minibuffer}}`
|
||||||
|
- `minibuffer` section kept in the template but not populated (empty context → section omitted)
|
||||||
|
|
||||||
|
## View model
|
||||||
|
|
||||||
|
A new function `spine-index-model` takes the book plists and an optional
|
||||||
|
selected-id, returns an `ht` hash table shaped for the template:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(spine-index-model books &optional selected-id) → ht
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app_title": "spine",
|
||||||
|
"total_books": 5,
|
||||||
|
"reading_count": 1,
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"label": "on deck",
|
||||||
|
"count": 3,
|
||||||
|
"books": [
|
||||||
|
{
|
||||||
|
"format_icon": "ti-book",
|
||||||
|
"status_class": "want",
|
||||||
|
"status_label": "want",
|
||||||
|
"title": "The Left Hand of Darkness",
|
||||||
|
"author": "Ursula K. Le Guin",
|
||||||
|
"tags_inline": null,
|
||||||
|
"rec_via": "Alex",
|
||||||
|
"rating_display": null,
|
||||||
|
"selected": false,
|
||||||
|
"detail": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field mappings
|
||||||
|
|
||||||
|
| Plist key | Template field | Transform |
|
||||||
|
|---|---|---|
|
||||||
|
| `:status` | `groups[].label` | WANT→"on deck", READING→"reading", READ→"read", ABANDONED→"abandoned" |
|
||||||
|
| `:status` | `status_class` / `status_label` | lowercased |
|
||||||
|
| `:format` | `format_icon` | hardcover→`ti-book`, ebook→`ti-device-tablet`, audiobook→`ti-headphones`, nil→`""` |
|
||||||
|
| `:tags` | `tags_inline` | `("scifi" "literary")` → `":scifi:literary:"` (string), nil→nil |
|
||||||
|
| `:rec_by` | `rec_via` | direct string, nil→nil |
|
||||||
|
| `:rating` | `rating_display` | "5"→"★★★★★", nil→nil |
|
||||||
|
| `:id` = selected-id | `selected` | boolean |
|
||||||
|
| `:notes` | `detail.notes` | list of `{date, text}` |
|
||||||
|
| `:added` + `:format` | `detail.meta` | composite string |
|
||||||
|
|
||||||
|
### Detail meta format
|
||||||
|
|
||||||
|
```
|
||||||
|
"started [2026-02-20] · hardcover" (WANT/READING)
|
||||||
|
"finished [2026-01-10] · ebook" (READ, use most recent LOGBOOK date)
|
||||||
|
"" (no dates)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group ordering
|
||||||
|
|
||||||
|
`on deck` (WANT) → `reading` (READING) → `read` (READ) → `abandoned` (ABANDONED).
|
||||||
|
Zero-book groups omitted.
|
||||||
|
|
||||||
|
## Handler
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defservlet index text/html (path query request)
|
||||||
|
(let ((books (spine-books)))
|
||||||
|
(if books
|
||||||
|
(insert (spine-render "index.mustache"
|
||||||
|
(spine-index-model books (cadr (assoc "id" query)))))
|
||||||
|
(insert (spine-render-empty-state)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
`spine-render-empty-state` keeps its current Pico-styled inline HTML
|
||||||
|
(no Mustache needed for a single static message).
|
||||||
|
|
||||||
|
## Templates directory
|
||||||
|
|
||||||
|
`spine-mockup-a-agenda.mustache` → `templates/index.mustache`
|
||||||
|
|
||||||
|
`spine-mockup-b-journal.mustache` stays in root as a reference/mockup
|
||||||
|
for the future detail view.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `templates/index.mustache` | New — moved from root mockup |
|
||||||
|
| `spine.el` | Add `spine-index-model`, update handler, remove old `spine-render-index` |
|
||||||
|
| `spine-mockup-a-agenda.mustache` | Deleted (moved) |
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Populating the `minibuffer` section (capture UI)
|
||||||
|
- Mockup B / journal detail view
|
||||||
|
- Cover image display
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- [ ] Index page renders via Mustache, not inline HTML
|
||||||
|
- [ ] Pico CSS + Tabler Icons CDN links present
|
||||||
|
- [ ] Groups: on deck, reading, read — correct order, zero-book groups omitted
|
||||||
|
- [ ] Format shown as Tabler icon glyph
|
||||||
|
- [ ] Status pills have distinct colors per state
|
||||||
|
- [ ] `?id=...` expands matching book detail
|
||||||
|
- [ ] Recommendation appears as "via {name}"
|
||||||
|
- [ ] ERT tests still pass (model layer unchanged)
|
||||||
Reference in New Issue
Block a user